@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.
- package/dist/authored-rule-intake-prewindow.test.js +22 -10
- package/dist/authored-rule-intake-prewindow.test.js.map +1 -1
- package/dist/authored-rule-intake.d.ts +50 -1
- package/dist/authored-rule-intake.d.ts.map +1 -1
- package/dist/authored-rule-intake.js +311 -22
- package/dist/authored-rule-intake.js.map +1 -1
- package/dist/commands/compile-noop-refresh.test.js +86 -1
- package/dist/commands/compile-noop-refresh.test.js.map +1 -1
- package/dist/commands/compile-refresh-manifest.test.js +38 -0
- package/dist/commands/compile-refresh-manifest.test.js.map +1 -1
- package/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +33 -6
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/doctor.d.ts +10 -2
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +22 -5
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +62 -0
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/lesson-archive.test.js +26 -1
- package/dist/commands/lesson-archive.test.js.map +1 -1
- package/dist/commands/lesson.d.ts.map +1 -1
- package/dist/commands/lesson.js +4 -1
- package/dist/commands/lesson.js.map +1 -1
- package/dist/commands/rule-author-command.test.js +15 -7
- package/dist/commands/rule-author-command.test.js.map +1 -1
- package/dist/commands/rule-author.test.js +584 -47
- package/dist/commands/rule-author.test.js.map +1 -1
- package/dist/commands/rule.d.ts.map +1 -1
- package/dist/commands/rule.js +67 -2
- package/dist/commands/rule.js.map +1 -1
- package/dist/commands/rule.test.js +128 -0
- package/dist/commands/rule.test.js.map +1 -1
- package/dist/commands/run-compiled-rules.d.ts.map +1 -1
- package/dist/commands/run-compiled-rules.js +54 -31
- package/dist/commands/run-compiled-rules.js.map +1 -1
- package/dist/commands/run-compiled-rules.test.js +100 -5
- package/dist/commands/run-compiled-rules.test.js.map +1 -1
- package/dist/commands/shield.d.ts.map +1 -1
- package/dist/commands/shield.js +3 -1
- package/dist/commands/shield.js.map +1 -1
- package/dist/commands/shield.test.js +50 -0
- package/dist/commands/shield.test.js.map +1 -1
- package/dist/commands/spine-authored-cert-corpus.d.ts.map +1 -1
- package/dist/commands/spine-authored-cert-corpus.js +5 -1
- package/dist/commands/spine-authored-cert-corpus.js.map +1 -1
- package/dist/commands/spine-authored-cert-corpus.test.js +50 -29
- package/dist/commands/spine-authored-cert-corpus.test.js.map +1 -1
- package/dist/commands/spine-corpus-disposition-source.d.ts +2 -2
- package/dist/commands/spine-freeze-orchestration.test.js +19 -21
- package/dist/commands/spine-freeze-orchestration.test.js.map +1 -1
- package/dist/commands/verify-manifest.d.ts.map +1 -1
- package/dist/commands/verify-manifest.js +59 -9
- package/dist/commands/verify-manifest.js.map +1 -1
- package/dist/commands/verify-manifest.test.js +243 -3
- package/dist/commands/verify-manifest.test.js.map +1 -1
- 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
|
-
|
|
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(
|
|
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
|
-
|
|
62
|
+
record: RECORD_REF,
|
|
51
63
|
positiveFixtures: [fixture(fixturePr)],
|
|
52
64
|
},
|
|
53
65
|
],
|
|
54
66
|
}), 'utf-8');
|
|
55
67
|
};
|
|
56
|
-
const run = (verified) => runRuleAuthor(
|
|
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":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,MAAM,CAAC;AAIlD,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE1D,oFAAoF;AACpF,kFAAkF;AAClF,4EAA4E;AAE5E,MAAM,SAAS,GAAG,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;AAC5C,MAAM,UAAU,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAElC,uFAAuF;AACvF,MAAM,QAAQ,GAAG;IACf,QAAQ,EAAE,SAAS;IACnB,gBAAgB,EAAE,UAAU;IAC5B,KAAK,EAAE,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE;CACtB,CAAC;AAEpC,IAAI,IAAY,CAAC;
|
|
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":"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
176
|
-
reason: `no unambiguous whitelist match for (${
|
|
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
|
-
|
|
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
|
|
465
|
+
declaredEngine,
|
|
186
466
|
structuralClass: r.structuralClass,
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
509
|
+
positiveFixtures,
|
|
230
510
|
...(r.negativeFixtures ? { negativeFixtures: r.negativeFixtures } : {}),
|
|
231
511
|
},
|
|
232
512
|
structuralEligibility,
|
|
233
513
|
origin,
|
|
234
|
-
declaredEngine
|
|
514
|
+
declaredEngine,
|
|
235
515
|
authoringLedgerRef: ruleId, // the ledger lineage for this ruleId; the latest entry is effective
|
|
236
|
-
|
|
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
|
|
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)).
|