@mmnto/cli 1.118.1 → 1.119.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/authored-rule-intake-prewindow.test.js +22 -10
  2. package/dist/authored-rule-intake-prewindow.test.js.map +1 -1
  3. package/dist/authored-rule-intake.d.ts +50 -1
  4. package/dist/authored-rule-intake.d.ts.map +1 -1
  5. package/dist/authored-rule-intake.js +311 -22
  6. package/dist/authored-rule-intake.js.map +1 -1
  7. package/dist/commands/compile-noop-refresh.test.js +86 -1
  8. package/dist/commands/compile-noop-refresh.test.js.map +1 -1
  9. package/dist/commands/compile-refresh-manifest.test.js +38 -0
  10. package/dist/commands/compile-refresh-manifest.test.js.map +1 -1
  11. package/dist/commands/compile.d.ts.map +1 -1
  12. package/dist/commands/compile.js +33 -6
  13. package/dist/commands/compile.js.map +1 -1
  14. package/dist/commands/doctor.d.ts +10 -2
  15. package/dist/commands/doctor.d.ts.map +1 -1
  16. package/dist/commands/doctor.js +22 -5
  17. package/dist/commands/doctor.js.map +1 -1
  18. package/dist/commands/doctor.test.js +62 -0
  19. package/dist/commands/doctor.test.js.map +1 -1
  20. package/dist/commands/lesson-archive.test.js +26 -1
  21. package/dist/commands/lesson-archive.test.js.map +1 -1
  22. package/dist/commands/lesson.d.ts.map +1 -1
  23. package/dist/commands/lesson.js +4 -1
  24. package/dist/commands/lesson.js.map +1 -1
  25. package/dist/commands/rule-author-command.test.js +15 -7
  26. package/dist/commands/rule-author-command.test.js.map +1 -1
  27. package/dist/commands/rule-author.test.js +584 -47
  28. package/dist/commands/rule-author.test.js.map +1 -1
  29. package/dist/commands/rule.d.ts.map +1 -1
  30. package/dist/commands/rule.js +67 -2
  31. package/dist/commands/rule.js.map +1 -1
  32. package/dist/commands/rule.test.js +128 -0
  33. package/dist/commands/rule.test.js.map +1 -1
  34. package/dist/commands/run-compiled-rules.d.ts.map +1 -1
  35. package/dist/commands/run-compiled-rules.js +54 -31
  36. package/dist/commands/run-compiled-rules.js.map +1 -1
  37. package/dist/commands/run-compiled-rules.test.js +100 -5
  38. package/dist/commands/run-compiled-rules.test.js.map +1 -1
  39. package/dist/commands/shield.d.ts.map +1 -1
  40. package/dist/commands/shield.js +3 -1
  41. package/dist/commands/shield.js.map +1 -1
  42. package/dist/commands/shield.test.js +50 -0
  43. package/dist/commands/shield.test.js.map +1 -1
  44. package/dist/commands/spine-authored-cert-corpus.d.ts.map +1 -1
  45. package/dist/commands/spine-authored-cert-corpus.js +5 -1
  46. package/dist/commands/spine-authored-cert-corpus.js.map +1 -1
  47. package/dist/commands/spine-authored-cert-corpus.test.js +50 -29
  48. package/dist/commands/spine-authored-cert-corpus.test.js.map +1 -1
  49. package/dist/commands/spine-corpus-disposition-source.d.ts +2 -2
  50. package/dist/commands/spine-freeze-orchestration.test.js +19 -21
  51. package/dist/commands/spine-freeze-orchestration.test.js.map +1 -1
  52. package/dist/commands/verify-manifest.d.ts.map +1 -1
  53. package/dist/commands/verify-manifest.js +59 -9
  54. package/dist/commands/verify-manifest.js.map +1 -1
  55. package/dist/commands/verify-manifest.test.js +243 -3
  56. package/dist/commands/verify-manifest.test.js.map +1 -1
  57. package/package.json +2 -2
@@ -16,26 +16,39 @@ const artifact = {
16
16
  split: { trainPrs: [447, 601], heldOutPrs: [602, 697] },
17
17
  };
18
18
  let root;
19
+ let totemDir;
20
+ // Prop 310 § Design 1: the rule half lives in a record file the envelope
21
+ // REFERENCES. The §5.2 gate reads `positiveFixtures[].pr`, which is unchanged.
22
+ const RECORD_REF = '.totem/rules/no-console-log.rule.yaml';
19
23
  beforeEach(() => {
20
24
  root = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-intake-prewindow-'));
21
- fs.mkdirSync(path.join(root, 'spine'), { recursive: true });
25
+ totemDir = path.join(root, '.totem');
26
+ fs.mkdirSync(path.join(totemDir, 'spine'), { recursive: true });
27
+ fs.mkdirSync(path.join(totemDir, 'rules'), { recursive: true });
28
+ fs.writeFileSync(path.join(totemDir, 'rules', 'no-console-log.rule.yaml'), yamlStringify({
29
+ schemaVersion: 1,
30
+ severity: 'warning',
31
+ message: 'console.log is banned in production code.',
32
+ target: {
33
+ type: 'regex',
34
+ pattern: 'console\\.log',
35
+ scope: { fileGlobs: ['src/**/*.rs'] },
36
+ },
37
+ examples: [{ bad: 'console.log("dbg")', good: 'logger.debug("dbg")' }],
38
+ }), 'utf-8');
22
39
  });
23
40
  afterEach(() => {
24
41
  fs.rmSync(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
25
42
  });
26
43
  const fixture = (pr) => ({
27
44
  pr,
28
- preimageSource: {
29
- kind: 'commit',
30
- preimageCommitSha: 'b'.repeat(40),
31
- mergeCommitSha: 'a'.repeat(40),
32
- },
33
45
  filePath: 'src/x.rs',
34
46
  matchedSpan: 'L1',
35
47
  contentHash: 'h'.repeat(8),
48
+ example: 0,
36
49
  });
37
50
  const writeYaml = (fixturePr) => {
38
- fs.writeFileSync(path.join(root, 'spine', 'authored-rules.yaml'), yamlStringify({
51
+ fs.writeFileSync(path.join(totemDir, 'spine', 'authored-rules.yaml'), yamlStringify({
39
52
  splitRef: SPLIT_REF,
40
53
  freezeCommitment: COMMITMENT,
41
54
  authoredAfterSplit: true,
@@ -45,15 +58,14 @@ const writeYaml = (fixturePr) => {
45
58
  author: 'alice',
46
59
  authoredAt: '2026-07-04',
47
60
  targetDefect: 'forbidden console.log',
48
- declaredEngine: 'regex',
49
61
  structuralClass: 'forbidden-literal-token',
50
- dslSource: 'console\\.log',
62
+ record: RECORD_REF,
51
63
  positiveFixtures: [fixture(fixturePr)],
52
64
  },
53
65
  ],
54
66
  }), 'utf-8');
55
67
  };
56
- const run = (verified) => runRuleAuthor(root, {
68
+ const run = (verified) => runRuleAuthor(totemDir, {
57
69
  judgedBy: 'static-whitelist@cert-1',
58
70
  freezeBinding: { artifact },
59
71
  ...(verified !== undefined ? { verifiedPreWindowFixturePrs: verified } : {}),
@@ -1 +1 @@
1
- {"version":3,"file":"authored-rule-intake-prewindow.test.js","sourceRoot":"","sources":["../src/authored-rule-intake-prewindow.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"authored-rule-intake-prewindow.test.js","sourceRoot":"","sources":["../src/authored-rule-intake-prewindow.test.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,55 @@
1
- import { type AuthoredRuleRecord, type FrozenSplitArtifact } from '@mmnto/totem';
1
+ import { type AuthoredFixture, type AuthoredRuleInput, type AuthoredRuleRecord, type FrozenSplitArtifact, type ParsedRuleRecord } from '@mmnto/totem';
2
2
  /** `.totem`-relative path to the authoring input. */
3
3
  export declare const AUTHORED_RULES_REL: string;
4
+ /** One ingested record: the resolved path, its LF-admitted content hash, and slice 1's parse. */
5
+ export interface IngestedRecord {
6
+ /** The reference exactly as the envelope wrote it — informational, never identity. */
7
+ path: string;
8
+ /** sha256 of the LF-admitted bytes — what every §8 attestation binds to. */
9
+ contentHash: string;
10
+ parsed: ParsedRuleRecord;
11
+ }
12
+ /**
13
+ * The closed set of § Design 1 shape rules an envelope `record:` reference can
14
+ * violate. A runtime value, not a bare union, so the test table can assert every
15
+ * rule has a negative fixture — the same discipline `GLOB_DIALECT_RULES` applies
16
+ * on the record-grammar side.
17
+ */
18
+ export declare const RECORD_REFERENCE_RULES: readonly ["separator", "absolute-path", "drive-letter", "unc-path", "empty-segment", "current-segment", "parent-segment", "records-dir-prefix", "suffix", "is-the-records-dir"];
19
+ export type RecordReferenceRule = (typeof RECORD_REFERENCE_RULES)[number];
20
+ /**
21
+ * LEXICAL validation of a `record:` reference — pure string rules over the value
22
+ * as written, run BEFORE any path resolution.
23
+ *
24
+ * Why lexical and why first (falsification leg MIN-2): `path.resolve` +
25
+ * `path.relative` reach the same verdict for a POSIX-clean reference on every
26
+ * platform, but they DIVERGE on the malformed ones. On win32 a backslash is a
27
+ * separator and on linux it is a filename character; a drive-letter absolute that
28
+ * happens to land inside the root passes containment; `//` collapses; and the
29
+ * default filesystem case-insensitivity lets `.totem/Rules/x.rule.yaml` resolve
30
+ * into the real directory on Windows and fail on linux. The reference is also
31
+ * recorded VERBATIM in the tracked `authoring-ledger.ndjson`, so an accepted
32
+ * absolute or backslashed form would commit a machine-specific path into a shared
33
+ * attestation. These rules give one verdict on both platforms, on the text.
34
+ *
35
+ * Returns the violated rule, or `null` when the reference is well-formed. The
36
+ * resolved-containment check in `resolveRecordPath` stays as the second line of
37
+ * defence.
38
+ */
39
+ export declare function checkRecordReference(totemDirName: string, reference: string): RecordReferenceRule | null;
40
+ /**
41
+ * Prop 310 § Design 10 / Amendment 1 — DERIVE each positive fixture's
42
+ * `preimageSource` from the record's `examples[ordinal]`, keyed `(ruleId, ordinal)`
43
+ * with the CR-blind pair hash as drift sensor.
44
+ *
45
+ * Called AFTER the identity is resolved, because the derived key carries the real
46
+ * minted `ruleId` — deriving before the mint would anchor the pair to a placeholder.
47
+ * A dangling ordinal and a duplicate ordinal both fail LOUD naming the rule and the
48
+ * ordinal ("a dangling, duplicate, or hash-mismatched reference fails loud at
49
+ * intake — never a silent re-pair"); the hash-mismatch leg is the ledger's own
50
+ * `contentHash` comparison, which needs no second mechanism.
51
+ */
52
+ export declare function deriveRecordFixtures(rule: AuthoredRuleInput, ingested: IngestedRecord, ruleId: string): AuthoredFixture[];
4
53
  /** A rule the whitelist could not decide — recorded, not minted, not ledgered (no record reaches compile). */
5
54
  export interface RejectedAuthoredRule {
6
55
  author: string;
@@ -1 +1 @@
1
- {"version":3,"file":"authored-rule-intake.d.ts","sourceRoot":"","sources":["../src/authored-rule-intake.ts"],"names":[],"mappings":"AAwBA,OAAO,EAGL,KAAK,kBAAkB,EAOvB,KAAK,mBAAmB,EAOzB,MAAM,cAAc,CAAC;AAItB,qDAAqD;AACrD,eAAO,MAAM,kBAAkB,QAA4C,CAAC;AA2C5E,8GAA8G;AAC9G,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,sFAAsF;IACtF,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,wFAAwF;IACxF,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,QAAQ,EAAE,oBAAoB,EAAE,CAAC;CAClC;AAUD;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE;QAAE,QAAQ,EAAE,mBAAmB,CAAA;KAAE,CAAC;IAClD;;;;;;;OAOG;IACH,2BAA2B,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACnD,GACA,gBAAgB,CA2TlB"}
1
+ {"version":3,"file":"authored-rule-intake.d.ts","sourceRoot":"","sources":["../src/authored-rule-intake.ts"],"names":[],"mappings":"AAkCA,OAAO,EAEL,KAAK,eAAe,EAEpB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EAOvB,KAAK,mBAAmB,EAGxB,KAAK,gBAAgB,EAQtB,MAAM,cAAc,CAAC;AAItB,qDAAqD;AACrD,eAAO,MAAM,kBAAkB,QAA4C,CAAC;AAkG5E,iGAAiG;AACjG,MAAM,WAAW,cAAc;IAC7B,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,iLAWzB,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAS1E;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,GAChB,mBAAmB,GAAG,IAAI,CAwB5B;AAoFD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,iBAAiB,EACvB,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,MAAM,GACb,eAAe,EAAE,CAqDnB;AAED,8GAA8G;AAC9G,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,sFAAsF;IACtF,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,wFAAwF;IACxF,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,QAAQ,EAAE,oBAAoB,EAAE,CAAC;CAClC;AAUD;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IACJ,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE;QAAE,QAAQ,EAAE,mBAAmB,CAAA;KAAE,CAAC;IAClD;;;;;;;OAOG;IACH,2BAA2B,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACnD,GACA,gBAAgB,CA8VlB"}
@@ -8,6 +8,15 @@
8
8
  // is the lazy-loaded surface (the `.coderabbit.yaml` lazy-load rule targets CLI
9
9
  // *commands* — CR's own suggested split for the synchronous producer).
10
10
  //
11
+ // PROP 310 slice 3 (§ Design 1, OQ-1 records-only): an entry no longer carries the
12
+ // rule inline. It carries a `record:` reference to `.totem/rules/<slug>.rule.yaml`,
13
+ // which this module resolves, reads through the LF admit hop, content-hashes, and
14
+ // parses with the V1 grammar. The engine the whitelist judges is DERIVED from the
15
+ // record's `target.type`; each positive fixture's certification `preimageSource` is
16
+ // DERIVED from `examples[<its ordinal>]` under the § Design 10 `(ruleId, ordinal)`
17
+ // key. An `dslSource` / `declaredEngine` / `preimageSource` key anywhere in the
18
+ // envelope is a MIGRATION error rejected by name.
19
+ //
11
20
  // The author may set ONLY author-owned fields (`AuthoredRuleInput` is `.strict()`,
12
21
  // and a recursive scan rejects producer-owned keys at ANY depth). The reader re-runs
13
22
  // `evaluateStructuralEligibility` over the DI whitelist (never in core, Tenet 5) and
@@ -16,10 +25,11 @@
16
25
  // rule throws before a single byte is written; pass 2 appends ledger rows (fail-loud
17
26
  // + read-back). SLICE B: the records are produced + ledgered but not yet fed to the
18
27
  // certifying corpus (B2 / C/D), which must key on `ruleId`, not `lessonHash`.
28
+ import { createHash } from 'node:crypto';
19
29
  import * as fs from 'node:fs';
20
30
  import * as path from 'node:path';
21
31
  import { parse as parseYaml } from 'yaml';
22
- import { appendAuthoringLedgerEntry, AuthoredRuleRecordSchema, AuthoredRulesFileSchema, authoringContentHash, buildAuthoredIdentityIndex, evaluateStructuralEligibility, identityKey, mintAuthoredRuleId, readAuthoringLedger, sanitizeForTerminal, SPLIT_REF_RE, TotemError, } from '@mmnto/totem';
32
+ import { appendAuthoringLedgerEntry, AuthoredRuleRecordSchema, AuthoredRulesFileSchema, authoringContentHash, buildAuthoredIdentityIndex, evaluateStructuralEligibility, identityKey, mintAuthoredRuleId, parseRuleRecord, readAuthoringLedger, RECORD_FILE_SUFFIX, RECORDS_DIR_REL, sanitizeForTerminal, SPLIT_REF_RE, TotemError, } from '@mmnto/totem';
23
33
  import { authoredWhitelist } from './commands/authored-whitelist.js';
24
34
  /** `.totem`-relative path to the authoring input. */
25
35
  export const AUTHORED_RULES_REL = path.join('spine', 'authored-rules.yaml');
@@ -43,9 +53,46 @@ const RESERVED_PRODUCER_KEYS = new Set([
43
53
  'routing',
44
54
  'unverified',
45
55
  ]);
46
- function assertNoReservedProducerKeys(value, at = '<root>') {
56
+ /**
57
+ * Prop 310 § Design 1 (OQ-1 ruling, records-only) — keys the PRE-slice-3 envelope
58
+ * carried that the record grammar now owns. Rejected BY NAME rather than as a
59
+ * generic unknown key, so the author is told WHERE the value went instead of that
60
+ * they typo'd a field:
61
+ *
62
+ * - `dslSource` / `declaredEngine` — the rule half moved into the record file;
63
+ * the engine is DERIVED there from `target.type` (§ Design 3/R17) and is never
64
+ * author-mirrored.
65
+ * - `preimageSource` — Amendment 1 makes the record's `examples` block the
66
+ * EDITABLE home and the envelope's side DERIVED, so hand-writing it would be
67
+ * the second copy the ruling exists to remove. The fixture references a pair by
68
+ * `example: <ordinal>` instead.
69
+ *
70
+ * Scanned at every depth for the same reason the producer set is: Zod `.strict()`
71
+ * is not recursive, so a nested occurrence would be STRIPPED rather than rejected.
72
+ */
73
+ const MIGRATED_RECORD_KEYS = new Map([
74
+ [
75
+ 'dslSource',
76
+ 'the rule itself now lives in a record file — move the matcher into `target:` there',
77
+ ],
78
+ [
79
+ 'declaredEngine',
80
+ 'the engine is DERIVED from the record’s `target.type` (Prop 310 § Design 3/R17), never declared on the envelope',
81
+ ],
82
+ [
83
+ 'preimageSource',
84
+ 'the bad/good pair now lives in the record’s `examples:` block and the envelope’s `preimageSource` is DERIVED from it (Prop 310 Amendment 1) — reference a pair with `example: <ordinal>` instead',
85
+ ],
86
+ ]);
87
+ /**
88
+ * `recordForm` is the reference shape THIS repo uses — derived from the caller's
89
+ * actual `totemDir`, never the hardcoded `.totem/`. A repo whose config names a
90
+ * different totem directory would otherwise be handed a migration instruction
91
+ * that does not resolve there (MIN-6).
92
+ */
93
+ function assertNoReservedProducerKeys(value, recordForm, at = '<root>') {
47
94
  if (Array.isArray(value)) {
48
- value.forEach((v, i) => assertNoReservedProducerKeys(v, `${at}[${i}]`));
95
+ value.forEach((v, i) => assertNoReservedProducerKeys(v, recordForm, `${at}[${i}]`));
49
96
  return;
50
97
  }
51
98
  if (value === null || typeof value !== 'object')
@@ -54,8 +101,218 @@ function assertNoReservedProducerKeys(value, at = '<root>') {
54
101
  if (RESERVED_PRODUCER_KEYS.has(k)) {
55
102
  throw new TotemError('CONFIG_INVALID', `authored-rules.yaml carries a producer-owned key '${k}' at ${at} — the producer establishes it, not the author (ADR-112 §3 / FM(d))`, 'Remove the field; the structural-eligibility verdict, rule id, and disposition are minted by the producer.');
56
103
  }
57
- assertNoReservedProducerKeys(v, `${at}.${k}`);
104
+ const migrated = MIGRATED_RECORD_KEYS.get(k);
105
+ if (migrated !== undefined) {
106
+ // The FULL key path, not just the parent: a migration message is an
107
+ // instruction to go edit one line, so it names the line.
108
+ throw new TotemError('CONFIG_INVALID', `authored-rules.yaml carries '${k}' at ${at}.${k} — ${migrated}. Since Prop 310 slice 3 an authored entry carries ONLY a \`record:\` reference to \`${recordForm}\` (Prop 310 § Design 1, OQ-1 records-only).`, `Move the rule into a record file at \`${recordForm}\` and replace the key with \`record: ${recordForm}\`.`);
109
+ }
110
+ assertNoReservedProducerKeys(v, recordForm, `${at}.${k}`);
111
+ }
112
+ }
113
+ /** The `record:` reference shape for a given `.totem` dir, as the author must write it. */
114
+ function recordReferenceForm(totemDir) {
115
+ return `${path.basename(totemDir)}/${RECORDS_DIR_REL}/<slug>${RECORD_FILE_SUFFIX}`;
116
+ }
117
+ /**
118
+ * The closed set of § Design 1 shape rules an envelope `record:` reference can
119
+ * violate. A runtime value, not a bare union, so the test table can assert every
120
+ * rule has a negative fixture — the same discipline `GLOB_DIALECT_RULES` applies
121
+ * on the record-grammar side.
122
+ */
123
+ export const RECORD_REFERENCE_RULES = [
124
+ 'separator',
125
+ 'absolute-path',
126
+ 'drive-letter',
127
+ 'unc-path',
128
+ 'empty-segment',
129
+ 'current-segment',
130
+ 'parent-segment',
131
+ 'records-dir-prefix',
132
+ 'suffix',
133
+ 'is-the-records-dir',
134
+ ];
135
+ // Vocabulary deliberately MIRRORS `checkGlobDialect`'s (rule-record.ts): a
136
+ // backslash is a `separator` violation, a leading `/` an `absolute-path`, a
137
+ // `C:` prefix a `drive-letter`, and so on, so an author who has met one surface
138
+ // reads the other without relearning it. `checkGlobDialect` itself is NOT called
139
+ // here — it bans `[ ( ) ? + | ^ $`, which are perfectly legal in a FILENAME.
140
+ const DRIVE_LETTER_RE = /^[A-Za-z]:/;
141
+ /**
142
+ * LEXICAL validation of a `record:` reference — pure string rules over the value
143
+ * as written, run BEFORE any path resolution.
144
+ *
145
+ * Why lexical and why first (falsification leg MIN-2): `path.resolve` +
146
+ * `path.relative` reach the same verdict for a POSIX-clean reference on every
147
+ * platform, but they DIVERGE on the malformed ones. On win32 a backslash is a
148
+ * separator and on linux it is a filename character; a drive-letter absolute that
149
+ * happens to land inside the root passes containment; `//` collapses; and the
150
+ * default filesystem case-insensitivity lets `.totem/Rules/x.rule.yaml` resolve
151
+ * into the real directory on Windows and fail on linux. The reference is also
152
+ * recorded VERBATIM in the tracked `authoring-ledger.ndjson`, so an accepted
153
+ * absolute or backslashed form would commit a machine-specific path into a shared
154
+ * attestation. These rules give one verdict on both platforms, on the text.
155
+ *
156
+ * Returns the violated rule, or `null` when the reference is well-formed. The
157
+ * resolved-containment check in `resolveRecordPath` stays as the second line of
158
+ * defence.
159
+ */
160
+ export function checkRecordReference(totemDirName, reference) {
161
+ if (reference.includes('\\'))
162
+ return 'separator';
163
+ if (reference.startsWith('//'))
164
+ return 'unc-path';
165
+ if (reference.startsWith('/'))
166
+ return 'absolute-path';
167
+ if (DRIVE_LETTER_RE.test(reference))
168
+ return 'drive-letter';
169
+ const requiredPrefix = `${totemDirName}/${RECORDS_DIR_REL}/`;
170
+ // CASE-SENSITIVE by construction (plain `startsWith`): the record class is the
171
+ // git-tracked path NAME, which is case-sensitive, and accepting a case variant
172
+ // here would admit a reference that resolves on a case-folding filesystem and
173
+ // dangles everywhere else.
174
+ if (!reference.startsWith(requiredPrefix))
175
+ return 'records-dir-prefix';
176
+ if (reference === requiredPrefix)
177
+ return 'is-the-records-dir';
178
+ for (const segment of reference.split('/')) {
179
+ if (segment.length === 0)
180
+ return 'empty-segment';
181
+ if (segment === '.')
182
+ return 'current-segment';
183
+ if (segment === '..')
184
+ return 'parent-segment';
185
+ }
186
+ // Last: a well-shaped path pointing at the wrong FILE CLASS is the most
187
+ // specific defect, so it reads after the structural ones.
188
+ if (!reference.endsWith(RECORD_FILE_SUFFIX))
189
+ return 'suffix';
190
+ return null;
191
+ }
192
+ /** Why each rule fired, in the author's terms. */
193
+ const RECORD_REFERENCE_DETAIL = {
194
+ separator: 'contains a backslash — records use `/` exclusively, on every platform',
195
+ 'absolute-path': 'is an absolute path — the reference is repo-relative',
196
+ 'drive-letter': 'carries a drive letter — the reference is repo-relative',
197
+ 'unc-path': 'is a UNC path — the reference is repo-relative',
198
+ 'empty-segment': 'contains an empty path segment',
199
+ 'current-segment': 'contains a `.` segment — write the path without it',
200
+ 'parent-segment': 'contains a `..` segment — a reference never navigates upward',
201
+ 'records-dir-prefix': 'does not begin with the records directory, compared CASE-SENSITIVELY against the git-tracked path name',
202
+ suffix: `does not end in '${RECORD_FILE_SUFFIX}'`,
203
+ 'is-the-records-dir': 'names the records directory itself rather than a record file',
204
+ };
205
+ /**
206
+ * Resolve an envelope `record:` reference to an absolute path.
207
+ *
208
+ * Two gates, in order: the platform-independent LEXICAL rules above, then the
209
+ * resolved-containment check as defence in depth.
210
+ *
211
+ * The reference is repo-relative (§ Design 1) and this library is git-free, so the
212
+ * repo root it resolves against is `dirname(totemDir)` — the layout `.totem/` and
213
+ * every fixture use. A nested `totemDir` would put the containment check out of
214
+ * reach of a repo-relative reference; that fails LOUD with the path named rather
215
+ * than silently resolving somewhere else.
216
+ */
217
+ function resolveRecordPath(totemDir, reference) {
218
+ const rulesRoot = path.resolve(totemDir, RECORDS_DIR_REL);
219
+ const totemDirName = path.basename(totemDir);
220
+ const expectedForm = recordReferenceForm(totemDir);
221
+ const reject = (rule, why) => {
222
+ throw new TotemError('CONFIG_INVALID', `authored-rules.yaml record reference '${reference}' ${why} [${rule}] — a record is referenced as \`${expectedForm}\` (Prop 310 § Design 1)`, `Write \`record: ${expectedForm}\` with forward slashes, no leading \`./\` or \`/\`, and the directory name exactly as it appears in git.`);
223
+ };
224
+ const violation = checkRecordReference(totemDirName, reference);
225
+ if (violation !== null)
226
+ reject(violation, RECORD_REFERENCE_DETAIL[violation]);
227
+ const resolved = path.resolve(path.dirname(totemDir), reference);
228
+ const relative = path.relative(rulesRoot, resolved);
229
+ // `..` as a whole SEGMENT, not as a prefix: a bare `startsWith('..')` also
230
+ // rejects `..hidden/x.rule.yaml` and `..a.rule.yaml`, which are ordinary file
231
+ // names that resolve INSIDE the root. The escape this guards is a path that
232
+ // steps OUT, and only a leading `..` segment does that.
233
+ const escapes = relative === '..' || relative.startsWith(`..${path.sep}`);
234
+ if (relative === '' || escapes || path.isAbsolute(relative)) {
235
+ reject('containment', `does not resolve inside ${rulesRoot}`);
58
236
  }
237
+ return resolved;
238
+ }
239
+ /** Read + parse one referenced record. A `RuleRecordParseError` propagates UNWRAPPED (§ Failure modes). */
240
+ function ingestRecord(totemDir, reference) {
241
+ const file = resolveRecordPath(totemDir, reference);
242
+ let raw;
243
+ try {
244
+ raw = fs.readFileSync(file, 'utf-8');
245
+ }
246
+ catch (err) {
247
+ throw new TotemError('CONFIG_MISSING', `rule record not found: ${file} (referenced as '${reference}' in authored-rules.yaml)`, `Create the record, or correct the \`record:\` reference (Prop 310 § Design 1).`, err);
248
+ }
249
+ // The LF image is the ONE representation everything downstream sees: the parse,
250
+ // the content hash, and the derived exemplars. That is the shipped admit hop
251
+ // (Amendment 1 item 2's `serialization-admit` class), and it is what makes a
252
+ // CRLF-authored record hash and derive identically to its LF twin.
253
+ const text = lf(raw);
254
+ const contentHash = createHash('sha256').update(text).digest('hex');
255
+ // NOT wrapped: `RuleRecordParseError` already names the file AND the offending
256
+ // record key path, and § Failure modes requires it reach the author intact rather
257
+ // than flattened into a generic reject.
258
+ const parsed = parseRuleRecord(text, reference);
259
+ return { path: reference, contentHash, parsed };
260
+ }
261
+ /**
262
+ * Prop 310 § Design 10 / Amendment 1 — DERIVE each positive fixture's
263
+ * `preimageSource` from the record's `examples[ordinal]`, keyed `(ruleId, ordinal)`
264
+ * with the CR-blind pair hash as drift sensor.
265
+ *
266
+ * Called AFTER the identity is resolved, because the derived key carries the real
267
+ * minted `ruleId` — deriving before the mint would anchor the pair to a placeholder.
268
+ * A dangling ordinal and a duplicate ordinal both fail LOUD naming the rule and the
269
+ * ordinal ("a dangling, duplicate, or hash-mismatched reference fails loud at
270
+ * intake — never a silent re-pair"); the hash-mismatch leg is the ledger's own
271
+ * `contentHash` comparison, which needs no second mechanism.
272
+ */
273
+ export function deriveRecordFixtures(rule, ingested, ruleId) {
274
+ const examples = ingested.parsed.record.examples;
275
+ const who = `${sanitizeForTerminal(rule.author)} · ${sanitizeForTerminal(rule.targetDefect)}`;
276
+ const seenOrdinals = new Set();
277
+ return rule.positiveFixtures.map((fixture) => {
278
+ const ordinal = fixture.example;
279
+ const example = examples[ordinal];
280
+ if (example === undefined) {
281
+ throw new TotemError('CONFIG_INVALID', `authored rule (${who}) declares a positive fixture referencing example ordinal ${ordinal}, but its record '${sanitizeForTerminal(ingested.path)}' carries ${examples.length} example pair(s) — the reference is DANGLING (Prop 310 § Design 10)`, `Use an ordinal in 0..${examples.length - 1}, or add the missing pair to the record's \`examples:\` block.`);
282
+ }
283
+ if (seenOrdinals.has(ordinal)) {
284
+ throw new TotemError('CONFIG_INVALID', `authored rule (${who}) declares two positive fixtures referencing the SAME example ordinal ${ordinal} — one exemplar pair cannot serve two corpus loci (Prop 310 § Design 10)`, "Author a second bad/good pair in the record's `examples:` block and point the second fixture at its ordinal.");
285
+ }
286
+ seenOrdinals.add(ordinal);
287
+ // Looked up by the `ordinal` FIELD, not by array position: `examplePairHashes`
288
+ // is a list of `{ordinal, hash}` records, so indexing it would silently bind a
289
+ // fixture to the wrong pair the moment the list is ever filtered or reordered —
290
+ // a wrong drift sensor is worse than none, because it reads as intact.
291
+ const pairHash = ingested.parsed.examplePairHashes.find((h) => h.ordinal === ordinal);
292
+ if (pairHash === undefined) {
293
+ // A PRODUCER bug, not an authoring defect: `parseRuleRecord` emits one hash
294
+ // per example. Reaching here means a hand-built `ParsedRuleRecord`, and
295
+ // recomputing the hash to paper over it would mint a § Design 10 drift sensor
296
+ // from a value nothing attested. Fail loud, in the incumbent idiom.
297
+ throw new Error(`[Totem Error] deriveRecordFixtures: rule '${ruleId}' has no example-pair hash for ordinal ${ordinal} — the parser emits one per \`examples[i]\` (Prop 310 Amendment 1 item 3), so a missing hash means the ParsedRuleRecord was hand-built or a producer dropped it; deriving the fixture anyway would give it a drift sensor nothing computed from the record.`);
298
+ }
299
+ return {
300
+ pr: fixture.pr,
301
+ filePath: fixture.filePath,
302
+ matchedSpan: fixture.matchedSpan,
303
+ contentHash: fixture.contentHash,
304
+ preimageSource: {
305
+ kind: 'record',
306
+ ruleId,
307
+ ordinal,
308
+ // The parser's own per-pair hash, never recomputed here: one home for the
309
+ // Amendment 1 item 3 basis (Tenet 20).
310
+ pairHash: pairHash.hash,
311
+ badExample: example.bad,
312
+ goodExample: example.good,
313
+ },
314
+ };
315
+ });
59
316
  }
60
317
  /**
61
318
  * ADR-112 §8 — ingest `.totem/spine/authored-rules.yaml` into authored records +
@@ -88,7 +345,7 @@ export function runRuleAuthor(totemDir, opts) {
88
345
  }
89
346
  // FM(d): reject reserved producer keys at ANY depth (codex — `.strict()` is not recursive),
90
347
  // THEN strict-parse the top level (any unknown top-level/rule key also fails, never stripped).
91
- assertNoReservedProducerKeys(doc);
348
+ assertNoReservedProducerKeys(doc, recordReferenceForm(totemDir));
92
349
  const parsed = AuthoredRulesFileSchema.safeParse(doc);
93
350
  if (!parsed.success) {
94
351
  throw new TotemError('CONFIG_INVALID', `authored-rules.yaml is invalid: ${parsed.error.message}`, 'The file may set only author-owned fields; producer fields (structuralEligibility, ruleId, decidable, disposition, …) are established by the producer, not the author (ADR-112 §3 / FM(d)).', parsed.error);
@@ -165,27 +422,56 @@ export function runRuleAuthor(totemDir, opts) {
165
422
  const pending = [];
166
423
  const rejected = [];
167
424
  for (const r of fileDoc.rules) {
425
+ // Prop 310 § Design 1: read + parse the referenced record FIRST — the engine the
426
+ // whitelist judges is DERIVED from it (§ Design 3/R17), so nothing downstream can
427
+ // run before the record is in hand. A grammar failure propagates as the parser's
428
+ // own typed error, naming the file and the offending key path.
429
+ const ingested = ingestRecord(totemDir, r.record);
430
+ const declaredEngine = ingested.parsed.derivedEngine;
168
431
  // Re-run the INDEPENDENT eligibility check; the author's structuralClass is a CLAIM.
169
- const structuralEligibility = evaluateStructuralEligibility({ declaredEngine: r.declaredEngine, structuralClass: r.structuralClass }, authoredWhitelist(), judgedBy);
432
+ const structuralEligibility = evaluateStructuralEligibility({ declaredEngine, structuralClass: r.structuralClass }, authoredWhitelist(), judgedBy);
170
433
  if (!structuralEligibility.decidable) {
171
434
  rejected.push({
172
435
  author: r.author,
173
436
  targetDefect: r.targetDefect,
174
437
  structuralClass: r.structuralClass,
175
- declaredEngine: r.declaredEngine,
176
- reason: `no unambiguous whitelist match for (${r.declaredEngine}, ${r.structuralClass}) — not structurally decidable (ADR-112 §3)`,
438
+ declaredEngine,
439
+ reason: `no unambiguous whitelist match for (${declaredEngine}, ${r.structuralClass}) — not structurally decidable (ADR-112 §3)`,
177
440
  });
178
441
  continue;
179
442
  }
180
443
  const origin = r.origin ?? FROM_SCRATCH;
181
- const dslSource = lf(r.dslSource);
444
+ // ── Identity BEFORE the material hash (Prop 310 slice 3 reorder) ──
445
+ // The derived `preimageSource` carries the minted `ruleId` as half of the
446
+ // § Design 10 join key, and the derived fixtures are material — so the identity
447
+ // has to be resolved first. The DECISION is unchanged: an existing identity
448
+ // reuses its persisted id and is judged by hash equality; only a genuinely new
449
+ // one mints. `ruleId` is stable per identity, so putting it inside the material
450
+ // cannot churn a rule that did not change.
451
+ const key = identityKey(r.author, r.targetDefect);
452
+ const existing = byIdentity.get(key);
453
+ let ruleId;
454
+ if (existing) {
455
+ ruleId = existing.ruleId; // reuse the persisted id — NEVER re-mint an existing identity
456
+ }
457
+ else {
458
+ ruleId = mintAuthoredRuleId(r.author, r.targetDefect, mintedIds);
459
+ mintedIds.add(ruleId);
460
+ }
461
+ const positiveFixtures = deriveRecordFixtures(r, ingested, ruleId);
182
462
  // The file-level attestations are part of the revision fingerprint (greptile-P1 + CR): an
183
463
  // attestation-only change (e.g. a re-frozen split) must append a fresh row, never read `unchanged`.
184
464
  const contentHash = authoringContentHash({
185
- declaredEngine: r.declaredEngine,
465
+ declaredEngine,
186
466
  structuralClass: r.structuralClass,
187
- dslSource,
188
- positiveFixtures: r.positiveFixtures,
467
+ // § Design 1: the record's BYTES are the material, its path is not — a rename
468
+ // changes no identity and must not force a re-attestation.
469
+ recordContentHash: ingested.contentHash,
470
+ // The DERIVED fixtures, not the author's ordinal references: the derivation
471
+ // carries the pair's text and its CR-blind hash, so an `examples` edit flips
472
+ // this fingerprint through the § Design 10 drift sensor as well as through
473
+ // the record's own content hash.
474
+ positiveFixtures,
189
475
  negativeFixtures: r.negativeFixtures,
190
476
  origin,
191
477
  splitRef: fileDoc.splitRef,
@@ -200,17 +486,11 @@ export function runRuleAuthor(totemDir, opts) {
200
486
  // Absent (legacy free-text splitRef) ⇒ dropped by canonicalStringify, hash unchanged.
201
487
  ...(binding !== undefined ? { freezeCommitment: binding.artifact.freezeCommitment } : {}),
202
488
  });
203
- const key = identityKey(r.author, r.targetDefect);
204
- const existing = byIdentity.get(key);
205
- let ruleId;
206
489
  let action;
207
490
  if (existing) {
208
- ruleId = existing.ruleId; // reuse the persisted id — NEVER re-mint an existing identity
209
491
  action = existing.contentHash === contentHash ? 'unchanged' : 'revised';
210
492
  }
211
493
  else {
212
- ruleId = mintAuthoredRuleId(r.author, r.targetDefect, mintedIds);
213
- mintedIds.add(ruleId);
214
494
  action = 'minted';
215
495
  }
216
496
  // Construct + validate the record. `authoredAt` calendar validity is enforced at intake
@@ -226,14 +506,18 @@ export function runRuleAuthor(totemDir, opts) {
226
506
  author: r.author,
227
507
  authoredAt: r.authoredAt,
228
508
  targetDefect: r.targetDefect,
229
- positiveFixtures: r.positiveFixtures,
509
+ positiveFixtures,
230
510
  ...(r.negativeFixtures ? { negativeFixtures: r.negativeFixtures } : {}),
231
511
  },
232
512
  structuralEligibility,
233
513
  origin,
234
- declaredEngine: r.declaredEngine,
514
+ declaredEngine,
235
515
  authoringLedgerRef: ruleId, // the ledger lineage for this ruleId; the latest entry is effective
236
- dslSource,
516
+ record: {
517
+ path: ingested.path,
518
+ contentHash: ingested.contentHash,
519
+ parsed: ingested.parsed,
520
+ },
237
521
  unverified: true,
238
522
  });
239
523
  }
@@ -245,12 +529,17 @@ export function runRuleAuthor(totemDir, opts) {
245
529
  author: r.author,
246
530
  targetDefect: r.targetDefect,
247
531
  authoredAt: r.authoredAt,
248
- declaredEngine: r.declaredEngine,
532
+ declaredEngine,
249
533
  splitRef: fileDoc.splitRef,
250
534
  authoredAfterSplit: fileDoc.authoredAfterSplit,
251
535
  heldOutNonInspectionAttestation: fileDoc.heldOutNonInspectionAttestation,
252
536
  structuralEligibility,
253
537
  origin,
538
+ // § Design 1 attestation relocation: the entry records WHICH bytes were
539
+ // ingested, so the §5 embargo proof stays content-addressed to the exact
540
+ // record set rather than weakening to an ambient claim. `path` rides along
541
+ // for diagnosis only — it is not in the material hash.
542
+ record: { path: ingested.path, contentHash: ingested.contentHash },
254
543
  positiveFixturePrs: r.positiveFixtures.map((f) => f.pr),
255
544
  // No negativeFixturePrs: §6 near-misses are silence-only with no `pr` (strategy#770 +
256
545
  // Q-C ruling); only positiveFixtures are train-side-attested (§5(2)).