Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .claude/board/entries/2026-11-03-d-grammar-constraints-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# D-GRAMMAR-CONSTRAINTS-0: grammatical constraints as typed relations over UD tokens (2026-11-03)

**Status:** measured, lab only. Probe `crates/grammar-constraints-probe` (own workspace); report `.claude/probes/2026-11-03-d-grammar-constraints/REPORT.md`. No production change, no new ClassId or `F###` word.

## Measured

- **UD German-GSD annotates two different roles identically.** The recipient of *geben* (31) and the experiencer of *gefallen* (18) are both `obl:arg` + `Case=Dat`. `iobj` occurs once in train. UD's German docs say this verbatim: "verbs of giving … have one oblique dative argument". So `Case=Dat → Recipient` is false, and so is `obl:arg+Dat → Recipient`.
- **Each role rule is one mask-risc program over DR-addressed token lanes.** The head hop is read in place with `Pred::EqU32Via` (fk = HEAD).
- Score against gold: 4/4 correct, 0 false positives, 0 false negatives, 1 abstention (UNKNOWN, not FALSE).
- The Direction/Purpose ambiguity is kept.
- 0 bytes allocated on the hot path; about 1.2–1.3 µs for 5 rules over 40 tokens.
- All 8 negative controls behave as predicted; 9 source disables go red.
- **P1 census:** the constraint shape is frame(head) × slot in German, Latin (PROIEL) and English (EWT), but the slot is language-specific.
- Latin `do`: recipient `obl:arg`+Dat (407).
- English `give`: recipient `iobj` (197) or `obl`+*to* (23).
- English `like`: the experiencer is `nsubj` (191).

## Open (owner)

- **First missing interface:** the DT64 WHEN (`SetExpr`) has no hop leaf, so the recipient and experiencer guards get equal witnesses. Even if the guard were statable, R2IL refuses it (`GuardNotExecutable`; 3VL is also refused).
- Executing the hop needs `EqU32Via`, whose call sites are EXO-140-unclassified.
- **Where the frame lexicon lives.** The language-specific frame lexicon is unversioned in any contract.
- **Which role vocabulary is canonical.** CE64 orientation has `Core` + 7; deepnsm-v2's `haspelmath` list has 8 and includes Judicantis.
- **Ambiguity cannot live in one CE64 word.** The orientation field holds one role; an alternative set needs masks or Bag candidates plus an explicit selection policy.
- **CG-3's REMOVE/SELECT is ordered and non-monotone** (the last-reading guard). It is effectful DO, not a derived relation. Parked.
3 changes: 2 additions & 1 deletion .claude/board/entries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately
opposite directions; the stranding this convention prevents shows up in
exactly one of them, never both.

565 entries, 2026-08-06 .. 2026-11-03.
566 entries, 2026-08-06 .. 2026-11-03.

| date | entry id | finding | file |
|---|---|---|---|
| 2026-11-03 | `D-XLOG-META-WIRE-P0` | | [2026-11-03-d-xlog-meta-wire-p0.md](2026-11-03-d-xlog-meta-wire-p0.md) |
| 2026-11-03 | `D-GRAMMAR-CONSTRAINTS-0` | | [2026-11-03-d-grammar-constraints-0.md](2026-11-03-d-grammar-constraints-0.md) |
| 2026-11-03 | `D-GLOBAL-WHERE` | | [2026-11-03-d-global-where-p0.md](2026-11-03-d-global-where-p0.md) |
| 2026-11-03 | `D-DR-FOLD-MAP` | | [2026-11-03-d-dr-fold-map.md](2026-11-03-d-dr-fold-map.md) |
| 2026-11-03 | `D-COGNITIVE-POLYFILL` | | [2026-11-03-d-cognitive-polyfill-p0.md](2026-11-03-d-cognitive-polyfill-p0.md) |
Expand Down
355 changes: 355 additions & 0 deletions .claude/probes/2026-11-03-d-grammar-constraints/REPORT.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ exclude = [
# D-COGNITIVE-POLYFILL P0 lab probe; own workspace root.
# `cargo test --manifest-path crates/cognitive-polyfill-probe/Cargo.toml`.
"crates/cognitive-polyfill-probe",
# D-GRAMMAR-CONSTRAINTS-0 lab probe; own workspace root.
# `cargo test --manifest-path crates/grammar-constraints-probe/Cargo.toml`.
"crates/grammar-constraints-probe",
"crates/symbiont",
# OGAR (Open Graph of Active Record) activation crate — re-exports OGAR's full
# AR surface (ogar-vocab Class/codebook + ogar-class-view impl ClassView +
Expand Down
21 changes: 21 additions & 0 deletions crates/grammar-constraints-probe/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "grammar-constraints-probe"
version = "0.0.0"
edition = "2021"
publish = false
description = "D-GRAMMAR-CONSTRAINTS-0 lab falsifier: German dative role candidates (recipient / experiencer / direction-purpose) as typed constraints over UD-annotated tokens, evaluated by one mask-risc program per rule over a DR-addressed token population. Lab only; no parser, no production path."

# Its OWN workspace root, like cognitive-polyfill-probe. Verify via
# cargo test --manifest-path crates/grammar-constraints-probe/Cargo.toml
[workspace]

[dependencies]
# DR identity: LocalMap / MapTable frames for the token population.
lance-graph-dt64 = { path = "../lance-graph-dt64" }
# The only bulk engine.
lance-graph-mask-risc = { path = "../lance-graph-mask-risc" }
# WHEN expressibility check (SetExpr / Guard normal form).
lance-graph-contract = { path = "../lance-graph-contract" }

[patch."https://github.com/AdaWorldAPI/ndarray"]
ndarray = { path = "../../../ndarray" }
53 changes: 53 additions & 0 deletions crates/grammar-constraints-probe/examples/cost.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//! Cold vs hot cost of the D-GRAMMAR-CONSTRAINTS-0 rules, reported separately.
//! cargo run --release --manifest-path crates/grammar-constraints-probe/Cargo.toml --example cost
//! Cold phases are one-off (reported, not gates); the hot run is the five
//! prepared programs over the 40-token corpus.

use std::time::Instant;

use grammar_constraints_probe::fixtures;
use grammar_constraints_probe::*;

fn ns(f: impl FnOnce()) -> u128 {
let t = Instant::now();
f();
t.elapsed().as_nanos()
}

fn main() {
let corpus = fixtures::corpus();
let gids = fixtures::gids(&corpus);
let mut vocab = Vocab::new();
let mut ids = None;
let t_vocab = ns(|| ids = Some(Ids::bind(&mut vocab)));
let ids = ids.unwrap();
let mut tf = None;
let t_dr = ns(|| tf = Some(tile(7, &gids)));
let (table, frame) = tf.unwrap();
let mut pop = None;
let t_pop = ns(|| pop = Some(Population::bind(&corpus, &mut vocab, &table, &frame).unwrap()));
let pop = pop.unwrap();
let mut rules = None;
let t_rules = ns(|| rules = Some(Rules::prepare(&ids, Operands::ALL)));
let mut rules = rules.unwrap();

for _ in 0..10_000 {
std::hint::black_box(rules.run(&pop, &table, &frame).unwrap());
}
let reps = 100_000u32;
let mut best = u128::MAX;
for _ in 0..5 {
let t = Instant::now();
for _ in 0..reps {
std::hint::black_box(
rules
.run(std::hint::black_box(&pop), &table, &frame)
.unwrap(),
);
}
best = best.min(t.elapsed().as_nanos() / u128::from(reps));
}
println!("tokens={} programs=5 mask_ops={}", pop.n, rules.op_count());
println!("cold (reported, not gates): vocab_bind={t_vocab} ns dr_map={t_dr} ns population_bind={t_pop} ns rule_prepare={t_rules} ns");
println!("hot: five programs incl. frame re-check = {best} ns/run (best of 5 x {reps})");
}
128 changes: 128 additions & 0 deletions crates/grammar-constraints-probe/src/fixtures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//! Hand-annotated sentences in UD German-GSD convention, and an independent
//! gold role per argument.
//!
//! The annotation convention was checked against the GSD treebank (UD
//! master, CC BY-SA 4.0; fetched for measurement, not committed):
//!
//! - a dative recipient of *geben* is `obl:arg` + `Case=Dat`
//! (`train-s1712`: "gab Shell Mitschuld", Shell `obl:arg` Dat);
//! - a dative experiencer of *gefallen* is ALSO `obl:arg` + `Case=Dat`
//! (`test-s258`: "das gefällt uns", uns `obl:arg` Dat);
//! - `zur` is a multiword token `zu` (`case`) + `der` (`det`, Dat), and its
//! noun is `obl` + `Case=Dat` under *bringen* ("zur Reparatur gebracht");
//! - `iobj` is not used for these (1 occurrence in GSD train, 0 in dev/test).
//!
//! Gold roles are a separate, single-annotator linguistic analysis (not
//! adjudicated), kept apart from the rules: they are what the rules are
//! scored against, never an input to them.

use crate::Tok;

macro_rules! t {
($gid:expr, $form:expr, $lemma:expr, $deprel:expr, $case:expr, $head:expr) => {
Tok {
gid: $gid,
form: $form,
lemma: $lemma,
deprel: $deprel,
case: $case,
head: $head,
}
};
}

/// A. Die Mutter gibt dem Kind das Buch.
pub const A: [Tok; 8] = [
t!(101, "Die", "der", "det", "Nom", 102),
t!(102, "Mutter", "Mutter", "nsubj", "Nom", 103),
t!(103, "gibt", "geben", "root", "", 0),
t!(104, "dem", "der", "det", "Dat", 105),
t!(105, "Kind", "Kind", "obl:arg", "Dat", 103),
t!(106, "das", "der", "det", "Acc", 107),
t!(107, "Buch", "Buch", "obj", "Acc", 103),
t!(108, ".", ".", "punct", "", 103),
];

/// B. Die Mutter bringt das Buch zur Schule. (`zur` = `zu` + `der`)
pub const B: [Tok; 9] = [
t!(201, "Die", "der", "det", "Nom", 202),
t!(202, "Mutter", "Mutter", "nsubj", "Nom", 203),
t!(203, "bringt", "bringen", "root", "", 0),
t!(204, "das", "der", "det", "Acc", 205),
t!(205, "Buch", "Buch", "obj", "Acc", 203),
t!(206, "zu", "zu", "case", "", 208),
t!(207, "der", "der", "det", "Dat", 208),
t!(208, "Schule", "Schule", "obl", "Dat", 203),
t!(209, ".", ".", "punct", "", 203),
];

/// C. Dem Kind gefällt das Buch.
pub const C: [Tok; 6] = [
t!(301, "Dem", "der", "det", "Dat", 302),
t!(302, "Kind", "Kind", "obl:arg", "Dat", 303),
t!(303, "gefällt", "gefallen", "root", "", 0),
t!(304, "das", "der", "det", "Nom", 305),
t!(305, "Buch", "Buch", "nsubj", "Nom", 303),
t!(306, ".", ".", "punct", "", 303),
];

/// D (control). Die Mutter gibt dem Kind mit dem Löffel den Brei.
/// `mit dem Löffel` is a dative `obl` under GIVE that is NOT a recipient.
pub const D: [Tok; 11] = [
t!(401, "Die", "der", "det", "Nom", 402),
t!(402, "Mutter", "Mutter", "nsubj", "Nom", 403),
t!(403, "gibt", "geben", "root", "", 0),
t!(404, "dem", "der", "det", "Dat", 405),
t!(405, "Kind", "Kind", "obl:arg", "Dat", 403),
t!(406, "mit", "mit", "case", "", 408),
t!(407, "dem", "der", "det", "Dat", 408),
t!(408, "Löffel", "Löffel", "obl", "Dat", 403),
t!(409, "den", "der", "det", "Acc", 410),
t!(410, "Brei", "Brei", "obj", "Acc", 403),
t!(411, ".", ".", "punct", "", 403),
];

/// E (control). Die Mutter hilft dem Kind. *helfen* has no frame entry.
pub const E: [Tok; 6] = [
t!(501, "Die", "der", "det", "Nom", 502),
t!(502, "Mutter", "Mutter", "nsubj", "Nom", 503),
t!(503, "hilft", "helfen", "root", "", 0),
t!(504, "dem", "der", "det", "Dat", 505),
t!(505, "Kind", "Kind", "obl:arg", "Dat", 503),
t!(506, ".", ".", "punct", "", 503),
];

/// Gold function of an argument token (Haspelmath dative-map names).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Gold {
Recipient,
Experiencer,
Direction,
/// A dative-map function the frame lexicon does not cover.
Beneficiary,
/// Not a dative-map function (e.g. an instrument).
NotOnMap,
}

/// The independent gold annotation (single annotator).
pub const GOLD: [(u64, Gold); 6] = [
(105, Gold::Recipient),
(208, Gold::Direction),
(302, Gold::Experiencer),
(405, Gold::Recipient),
(408, Gold::NotOnMap),
(505, Gold::Beneficiary),
];

/// Every sentence, in corpus order.
pub fn corpus() -> Vec<Tok> {
let mut v = Vec::new();
for s in [&A[..], &B[..], &C[..], &D[..], &E[..]] {
v.extend_from_slice(s);
}
v
}

pub fn gids(toks: &[Tok]) -> Vec<u64> {
toks.iter().map(|t| t.gid).collect()
}
Loading
Loading