@mmnto/totem 1.118.1 → 1.120.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/artifacts/admission.d.ts +12 -12
- package/dist/artifacts/verdict.d.ts +16 -16
- package/dist/compile-manifest.d.ts +89 -0
- package/dist/compile-manifest.d.ts.map +1 -1
- package/dist/compile-manifest.js +180 -31
- package/dist/compile-manifest.js.map +1 -1
- package/dist/compile-manifest.test.js +169 -1
- package/dist/compile-manifest.test.js.map +1 -1
- package/dist/compile-smoke-gate.d.ts +23 -0
- package/dist/compile-smoke-gate.d.ts.map +1 -1
- package/dist/compile-smoke-gate.js +139 -8
- package/dist/compile-smoke-gate.js.map +1 -1
- package/dist/compile-smoke-gate.test.js +431 -0
- package/dist/compile-smoke-gate.test.js.map +1 -1
- package/dist/compiler-schema.d.ts +1067 -0
- package/dist/compiler-schema.d.ts.map +1 -1
- package/dist/compiler-schema.js +204 -1
- package/dist/compiler-schema.js.map +1 -1
- package/dist/compiler-schema.test.js +92 -0
- package/dist/compiler-schema.test.js.map +1 -1
- package/dist/compiler.d.ts +2 -6
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +6 -17
- package/dist/compiler.js.map +1 -1
- package/dist/freeze.d.ts +3 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +12 -12
- package/dist/recurrence-stats.d.ts +10 -10
- package/dist/regex-safety/apply-rules-bounded.d.ts +11 -0
- package/dist/regex-safety/apply-rules-bounded.d.ts.map +1 -1
- package/dist/regex-safety/apply-rules-bounded.js +59 -5
- package/dist/regex-safety/apply-rules-bounded.js.map +1 -1
- package/dist/regex-validation.d.ts +7 -0
- package/dist/regex-validation.d.ts.map +1 -0
- package/dist/regex-validation.js +32 -0
- package/dist/regex-validation.js.map +1 -0
- package/dist/rule-engine.d.ts +11 -1
- package/dist/rule-engine.d.ts.map +1 -1
- package/dist/rule-engine.js +125 -18
- package/dist/rule-engine.js.map +1 -1
- package/dist/spine/authored-rule.d.ts +687 -263
- package/dist/spine/authored-rule.d.ts.map +1 -1
- package/dist/spine/authored-rule.js +113 -27
- package/dist/spine/authored-rule.js.map +1 -1
- package/dist/spine/authored-rule.test.js +170 -4
- package/dist/spine/authored-rule.test.js.map +1 -1
- package/dist/spine/authoring-ledger.d.ts +83 -24
- package/dist/spine/authoring-ledger.d.ts.map +1 -1
- package/dist/spine/authoring-ledger.js +46 -25
- package/dist/spine/authoring-ledger.js.map +1 -1
- package/dist/spine/authoring-ledger.test.js +20 -7
- package/dist/spine/authoring-ledger.test.js.map +1 -1
- package/dist/spine/candidate-rule.d.ts +18 -1
- package/dist/spine/candidate-rule.d.ts.map +1 -1
- package/dist/spine/candidate-rule.js.map +1 -1
- package/dist/spine/cert-corpus-seed.d.ts +14 -14
- package/dist/spine/compile.d.ts +4 -1
- package/dist/spine/compile.d.ts.map +1 -1
- package/dist/spine/compile.js +50 -3
- package/dist/spine/compile.js.map +1 -1
- package/dist/spine/compile.test.js +189 -23
- package/dist/spine/compile.test.js.map +1 -1
- package/dist/spine/frozen-split.d.ts +18 -18
- package/dist/spine/ledgers.d.ts +62 -62
- package/dist/spine/lf-normalize.d.ts +9 -0
- package/dist/spine/lf-normalize.d.ts.map +1 -0
- package/dist/spine/lf-normalize.js +38 -0
- package/dist/spine/lf-normalize.js.map +1 -0
- package/dist/spine/preimage-differential.d.ts +3 -1
- package/dist/spine/preimage-differential.d.ts.map +1 -1
- package/dist/spine/preimage-differential.js +9 -6
- package/dist/spine/preimage-differential.js.map +1 -1
- package/dist/spine/preimage-differential.test.js +59 -0
- package/dist/spine/preimage-differential.test.js.map +1 -1
- package/dist/spine/record-exemplars.fixture.d.ts +5 -0
- package/dist/spine/record-exemplars.fixture.d.ts.map +1 -0
- package/dist/spine/record-exemplars.fixture.js +61 -0
- package/dist/spine/record-exemplars.fixture.js.map +1 -0
- package/dist/spine/record-lower.d.ts +90 -0
- package/dist/spine/record-lower.d.ts.map +1 -0
- package/dist/spine/record-lower.js +418 -0
- package/dist/spine/record-lower.js.map +1 -0
- package/dist/spine/record-lower.test.d.ts +2 -0
- package/dist/spine/record-lower.test.d.ts.map +1 -0
- package/dist/spine/record-lower.test.js +562 -0
- package/dist/spine/record-lower.test.js.map +1 -0
- package/dist/spine/record-runtime.d.ts +215 -0
- package/dist/spine/record-runtime.d.ts.map +1 -0
- package/dist/spine/record-runtime.js +330 -0
- package/dist/spine/record-runtime.js.map +1 -0
- package/dist/spine/record-runtime.test.d.ts +2 -0
- package/dist/spine/record-runtime.test.d.ts.map +1 -0
- package/dist/spine/record-runtime.test.js +814 -0
- package/dist/spine/record-runtime.test.js.map +1 -0
- package/dist/spine/rule-record.d.ts +864 -0
- package/dist/spine/rule-record.d.ts.map +1 -0
- package/dist/spine/rule-record.js +781 -0
- package/dist/spine/rule-record.js.map +1 -0
- package/dist/spine/rule-record.test.d.ts +2 -0
- package/dist/spine/rule-record.test.d.ts.map +1 -0
- package/dist/spine/rule-record.test.js +1262 -0
- package/dist/spine/rule-record.test.js.map +1 -0
- package/dist/spine/windtunnel-firing.d.ts.map +1 -1
- package/dist/spine/windtunnel-firing.js +28 -1
- package/dist/spine/windtunnel-firing.js.map +1 -1
- package/dist/spine/windtunnel-lock.d.ts +26 -26
- package/dist/stage4-verifier.d.ts.map +1 -1
- package/dist/stage4-verifier.js +61 -17
- package/dist/stage4-verifier.js.map +1 -1
- package/dist/sys/glob.d.ts +30 -0
- package/dist/sys/glob.d.ts.map +1 -1
- package/dist/sys/glob.js +62 -1
- package/dist/sys/glob.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { CompiledRule } from '../compiler-schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* The Prop 310 compiled homes, as a runtime-readable list. A rule carrying ANY
|
|
4
|
+
* of them came from the record path; a rule carrying NONE is legacy.
|
|
5
|
+
*
|
|
6
|
+
* DERIVED, never mirrored (Tenet 20): the discriminator is the presence of the
|
|
7
|
+
* § Design 12 homes themselves, not a separate `dialect:` marker field — a marker
|
|
8
|
+
* would be a second, drift-capable statement of what the fields already say, and
|
|
9
|
+
* § Design 4's key space is closed against inventing one.
|
|
10
|
+
*
|
|
11
|
+
* The derivation is SAFE because nothing but the record path can write these
|
|
12
|
+
* fields: they were added to `CompiledRuleSchema` by this slice, and the only
|
|
13
|
+
* other writer (the legacy lesson-compile actuator) is under the standing
|
|
14
|
+
* `rule-compilation` freeze and does not regenerate. The byte-identity guard
|
|
15
|
+
* asserts the empirical half — zero of the shipped 485 rules carries any of them.
|
|
16
|
+
*/
|
|
17
|
+
export declare const RECORD_COMPILED_HOME_KEYS: readonly ["excludeGlobs", "requires", "examples", "language", "verificationShadow", "recoveryHint", "curation"];
|
|
18
|
+
/**
|
|
19
|
+
* True when a compiled rule came from the Prop 310 record path — i.e. when the
|
|
20
|
+
* § Design 7 dialect and its two-array scope rule apply to it. False for every
|
|
21
|
+
* legacy/mined rule, which keeps the shipped matcher unchanged.
|
|
22
|
+
*
|
|
23
|
+
* The test is `examples !== undefined` ALONE, deliberately narrower than "any
|
|
24
|
+
* § Design 12 home" (falsification round, 2026-08-21). `examples` is the EXACT
|
|
25
|
+
* discriminator, not merely a sufficient one:
|
|
26
|
+
* - § Design 5 makes it non-omittable and min-1, so EVERY record carries it;
|
|
27
|
+
* - `compileRuleRecord` emits it unconditionally, so every lowered rule has it;
|
|
28
|
+
* - it is the one home no other construct can be present without.
|
|
29
|
+
*
|
|
30
|
+
* The wider seven-key disjunction was demonstrated to be UNSAFE in the other
|
|
31
|
+
* direction: a legacy-shaped rule that ever gained just a `recoveryHint` — a
|
|
32
|
+
* plausible future addition to the mined path, and a purely cosmetic one — would
|
|
33
|
+
* silently flip from the shipped tree-wide matcher to the record dialect's
|
|
34
|
+
* root-only reading of the same `*.ts` glob, narrowing its scope with no signal.
|
|
35
|
+
* Keying on the field that MEANS "this is a record" removes that coupling
|
|
36
|
+
* between a diagnostic field and a matcher semantic.
|
|
37
|
+
*
|
|
38
|
+
* `RECORD_COMPILED_HOME_KEYS` remains the full home set for the corpus guard
|
|
39
|
+
* (which asserts the legacy manifest carries NONE of them — a stronger claim
|
|
40
|
+
* than this predicate needs, and the right one for a freeze check).
|
|
41
|
+
*/
|
|
42
|
+
export declare function isRecordPathRule(rule: Pick<CompiledRule, 'examples'>): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The exemplar lines a rule's Stage-4 / doctor readers treat as "the authored bad
|
|
45
|
+
* example", single-homed (Tenet 20).
|
|
46
|
+
*
|
|
47
|
+
* Prop 310 slice 3 discharges the slice-2 pin: a record rule's authored preimages
|
|
48
|
+
* live in `examples[i].bad` — the § Design 10 EDITABLE home — and are NOT mirrored
|
|
49
|
+
* onto the legacy `badExample` field, so every reader of that field would otherwise
|
|
50
|
+
* read a record rule as having no bad example at all. Both shipped readers now ask
|
|
51
|
+
* this one function instead:
|
|
52
|
+
*
|
|
53
|
+
* - Stage 4 (`lineMatchesBadExample`) — a matched line equal to any of these
|
|
54
|
+
* classifies `in-scope-bad-example` rather than `candidate-debt`;
|
|
55
|
+
* - `totem doctor`'s `no-badExample` grandfathering reason — an empty result is
|
|
56
|
+
* exactly the old `!badExample || badExample.trim() === ''` test.
|
|
57
|
+
*
|
|
58
|
+
* BLANK LINES ARE DROPPED, which is what makes both of those byte-identical for
|
|
59
|
+
* legacy rules: Stage 4 already refuses to match an empty trimmed line, and a
|
|
60
|
+
* whitespace-only `badExample` must still count as absent for doctor.
|
|
61
|
+
*/
|
|
62
|
+
export declare function ruleBadExampleLines(rule: Pick<CompiledRule, 'badExample' | 'examples'>): string[];
|
|
63
|
+
/**
|
|
64
|
+
* The POSTIMAGE twin of `ruleBadExampleLines`, on the identical contract: record
|
|
65
|
+
* path → every `examples[i].good`, legacy → `goodExample`, blank lines dropped.
|
|
66
|
+
*
|
|
67
|
+
* `totem doctor`'s `no-goodExample` grandfathering reason reads it. Without the
|
|
68
|
+
* twin, a record rule cleared the `no-badExample` reason and then tripped
|
|
69
|
+
* `no-goodExample` on the very same absence — the record's exemplars live in
|
|
70
|
+
* `examples`, and § Design 10 makes that the editable home for BOTH sides of the
|
|
71
|
+
* pair, so a reader that single-homes one side and not the other reports half a
|
|
72
|
+
* truth. An empty result is exactly the old
|
|
73
|
+
* `!goodExample || goodExample.trim() === ''` test.
|
|
74
|
+
*/
|
|
75
|
+
export declare function ruleGoodExampleLines(rule: Pick<CompiledRule, 'goodExample' | 'examples'>): string[];
|
|
76
|
+
/**
|
|
77
|
+
* Exactly the fields the scope predicates READ. Structural rather than the whole
|
|
78
|
+
* `CompiledRule` so callers holding a projection — Stage 4's `classifyFile`
|
|
79
|
+
* takes one — pass it directly instead of casting a narrower object back to the
|
|
80
|
+
* full type. A cast there would have been a lie the compiler stopped checking.
|
|
81
|
+
*/
|
|
82
|
+
export type RuleScopeFields = Pick<CompiledRule, 'fileGlobs' | 'excludeGlobs' | 'examples'>;
|
|
83
|
+
/**
|
|
84
|
+
* § Design 12 — fail loud on a TORN record rule: one carrying a Prop 310
|
|
85
|
+
* compiled home but no `examples`.
|
|
86
|
+
*
|
|
87
|
+
* Such a rule cannot exist downstream of the lowering (`examples` is min-1 at
|
|
88
|
+
* parse and emitted unconditionally) nor in the frozen legacy corpus (which
|
|
89
|
+
* carries no home at all), so this only ever catches a hand-edited manifest or
|
|
90
|
+
* a bypassed producer. Left unchecked, `isRecordPathRule` would class it LEGACY
|
|
91
|
+
* and its `excludeGlobs` and `requires` would be dropped — a scope silently
|
|
92
|
+
* widened and a requirement silently unevaluated. The drop direction is toward
|
|
93
|
+
* flagging, so it is not fail-open, but § Design 12's ban on accepting a
|
|
94
|
+
* construct and dropping it is a ban on the SILENCE, not just on the unsafe
|
|
95
|
+
* direction. Fail loud instead.
|
|
96
|
+
*
|
|
97
|
+
* Called at dispatcher altitude, once per invocation — the same altitude as the
|
|
98
|
+
* tree-sitter `requires` backstop, and for the same reason: a per-file check
|
|
99
|
+
* would re-report the same defect on every file.
|
|
100
|
+
*/
|
|
101
|
+
export declare function assertNoTornRecordRules(rules: readonly CompiledRule[]): void;
|
|
102
|
+
/**
|
|
103
|
+
* § Design 7 — the two-array scope rule for a RECORD-path rule:
|
|
104
|
+
* `positiveMatch && !excludeMatch`, both arrays evaluated under the normative
|
|
105
|
+
* dialect (`matchesRecordGlob`).
|
|
106
|
+
*
|
|
107
|
+
* `fileGlobs` is min-1 at parse, so the empty-positive case cannot arise from a
|
|
108
|
+
* well-formed record; a hand-edited manifest that produced one would match
|
|
109
|
+
* NOTHING here rather than everything. That direction is deliberate — the
|
|
110
|
+
* shipped `fileMatchesGlobs` treats "no positive globs" as include-everything,
|
|
111
|
+
* which for a record-path rule would silently WIDEN a scope the grammar requires
|
|
112
|
+
* to be declared. A rule that fires nowhere is visible; one that fires everywhere
|
|
113
|
+
* is the fail-open class § Design 2 exists to kill.
|
|
114
|
+
*/
|
|
115
|
+
export declare function recordScopeMatchesFile(filePath: string, fileGlobs: readonly string[] | undefined, excludeGlobs: readonly string[] | undefined): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* The ONE scope predicate the rule engine asks: does this rule apply to this
|
|
118
|
+
* file? Record-path rules get § Design 7's dialect + two-array rule; every other
|
|
119
|
+
* rule gets the shipped `fileMatchesGlobs` predicate verbatim, including its
|
|
120
|
+
* "unscoped rule applies to everything" behaviour.
|
|
121
|
+
*
|
|
122
|
+
* Single-homed on purpose: the engine had four separate copies of the
|
|
123
|
+
* `rule.fileGlobs && rule.fileGlobs.length > 0 ? fileMatchesGlobs(...) : true`
|
|
124
|
+
* expression, and four copies of a dialect fork is how a rule silently gets one
|
|
125
|
+
* scope at dispatch and a different one at the fail-loud guard.
|
|
126
|
+
*/
|
|
127
|
+
export declare function ruleAppliesToFile(rule: RuleScopeFields, filePath: string): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* The text the § Design 8 scopes resolve to at a target-match locus L.
|
|
130
|
+
*
|
|
131
|
+
* `file` is a LAZY resolver returning `null` when the file cannot be read: the
|
|
132
|
+
* caller owns the IO, a `line`-scope requirement never triggers a read, and the
|
|
133
|
+
* unreadable case gets an explicit, documented direction below rather than an
|
|
134
|
+
* exception thrown from inside a matcher.
|
|
135
|
+
*/
|
|
136
|
+
export interface RequiresScopeText {
|
|
137
|
+
/** L's line — the text of the line the target matched on. */
|
|
138
|
+
line: string;
|
|
139
|
+
/** The whole file containing L, or `null` when unreadable. */
|
|
140
|
+
file: () => string | null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Path-containment check for the § Design 8 whole-file readers.
|
|
144
|
+
*
|
|
145
|
+
* The paths handed to these readers come out of a DIFF, which is
|
|
146
|
+
* attacker-shaped input on any path where a lint runs over untrusted contributed
|
|
147
|
+
* changes — a `../` prefix would otherwise read outside the repo. `path.relative`
|
|
148
|
+
* rather than `startsWith` on purpose: the string form admits the
|
|
149
|
+
* sibling-directory bypass (`/app-secrets` passing a `/app` prefix test), which
|
|
150
|
+
* is the same reasoning the shipped ast-path containment checks use.
|
|
151
|
+
*
|
|
152
|
+
* An out-of-root path is reported as UNREADABLE, not as an error: the requirement
|
|
153
|
+
* is then unmet and the rule FIRES, preserving the fail-toward-flagging direction
|
|
154
|
+
* every other read failure on this path takes.
|
|
155
|
+
*/
|
|
156
|
+
export declare function isInsideRoot(root: string, filePath: string): boolean;
|
|
157
|
+
/**
|
|
158
|
+
* § Design 8 — gate every `requires.pattern` in the loaded manifest through the
|
|
159
|
+
* SAME safe-regex2 check the compile path applies, before any of them is
|
|
160
|
+
* evaluated.
|
|
161
|
+
*
|
|
162
|
+
* The lowering already gates this, so a pattern reaching here unsafe means the
|
|
163
|
+
* manifest was hand-edited or written by a bypassed producer. Left ungated it is
|
|
164
|
+
* a live ReDoS surface, and a worse one than the target pattern's: at
|
|
165
|
+
* `scope: file` the requirement runs against WHOLE-FILE text, unbounded, on the
|
|
166
|
+
* pre-commit path. `validateRegex` is the same single-homed check the compile
|
|
167
|
+
* gate uses, so the two can never drift.
|
|
168
|
+
*
|
|
169
|
+
* Dispatcher altitude, once per invocation — the same reason as
|
|
170
|
+
* `assertNoTornRecordRules`: a per-match check would re-report one defect on
|
|
171
|
+
* every line of every file.
|
|
172
|
+
*/
|
|
173
|
+
export declare function assertRequiresPatternsSafe(rules: readonly CompiledRule[]): void;
|
|
174
|
+
/**
|
|
175
|
+
* § Design 8 — fail loud on `ast-grep` + `requires.scope: line`.
|
|
176
|
+
*
|
|
177
|
+
* The lowering REJECTS this combination (an ast-grep match is a span, and
|
|
178
|
+
* reading it as its start line makes the verdict depend on source formatting),
|
|
179
|
+
* but a hand-edited manifest can still carry it, and the ast dispatcher would
|
|
180
|
+
* silently evaluate `match.lineText` — shipping exactly the formatting-dependent
|
|
181
|
+
* semantic the ruling refused. Mirrors the tree-sitter `requires` backstop:
|
|
182
|
+
* unreachable from any compiled record, unreachable from the legacy corpus,
|
|
183
|
+
* live only against a torn manifest.
|
|
184
|
+
*/
|
|
185
|
+
export declare function assertNoAstGrepLineScope(rules: readonly CompiledRule[]): void;
|
|
186
|
+
/**
|
|
187
|
+
* § Design 8 — pass two. Returns TRUE when the required context is PRESENT
|
|
188
|
+
* within the declared scope containing L, i.e. when the rule must stay SILENT.
|
|
189
|
+
* The rule fires iff the target matched AND this returns false.
|
|
190
|
+
*
|
|
191
|
+
* `requires.pattern` is a regex evaluated TEXTUALLY, independent of the target
|
|
192
|
+
* engine — "the requirement is a context check, not a second matcher" — so an
|
|
193
|
+
* ast-grep rule's requirement is still a plain regex over the scope text.
|
|
194
|
+
*
|
|
195
|
+
* UNREADABLE FILE (`scope: file` only) ⇒ context ABSENT ⇒ the rule FIRES. The
|
|
196
|
+
* safe direction for a lint gate is toward flagging: a false positive is visible
|
|
197
|
+
* and disputable, a suppressed real violation is silent. This mirrors the shipped
|
|
198
|
+
* Rust test-span exemption, whose read failure likewise yields no exemption
|
|
199
|
+
* (`rule-engine.ts`: "the exemption fails toward flagging, never toward
|
|
200
|
+
* suppression").
|
|
201
|
+
*
|
|
202
|
+
* A `requires.pattern` that will not compile means the compile-stage gate was
|
|
203
|
+
* bypassed or the manifest was hand-edited. Fail LOUD — the same treatment
|
|
204
|
+
* `applyRulesToAdditions` gives an uncompilable `rule.pattern`; silently treating
|
|
205
|
+
* it as "context absent" would turn a broken rule into a firing one.
|
|
206
|
+
*/
|
|
207
|
+
export declare function requiresContextPresent(rule: CompiledRule, requires: NonNullable<CompiledRule['requires']>, text: RequiresScopeText): boolean;
|
|
208
|
+
/**
|
|
209
|
+
* § Design 8 applied to one target match: TRUE when the rule must stay silent at
|
|
210
|
+
* this locus because its required context is present. Always false for a rule
|
|
211
|
+
* carrying no `requires` block — which is every legacy rule, so the shipped
|
|
212
|
+
* evaluation path is untouched by construction.
|
|
213
|
+
*/
|
|
214
|
+
export declare function requiresSuppressesMatch(rule: CompiledRule, text: RequiresScopeText): boolean;
|
|
215
|
+
//# sourceMappingURL=record-runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-runtime.d.ts","sourceRoot":"","sources":["../../src/spine/record-runtime.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAK1D;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,yBAAyB,iHAQc,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,GAAG,OAAO,CAE9E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,GAAG,UAAU,CAAC,GAAG,MAAM,EAAE,CAIjG;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,IAAI,CAAC,YAAY,EAAE,aAAa,GAAG,UAAU,CAAC,GACnD,MAAM,EAAE,CAMV;AAkBD;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,YAAY,EAAE,WAAW,GAAG,cAAc,GAAG,UAAU,CAAC,CAAC;AAE5F;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,IAAI,CAY5E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,EACxC,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAC1C,OAAO,CAKT;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQlF;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,IAAI,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CAC3B;AAUD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIpE;AAUD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,IAAI,CAU/E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,IAAI,CAS7E;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,WAAW,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,EAC/C,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAqBT;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAG5F"}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// ─── Prop 310 V1 record grammar — RUNTIME EVALUATION (slice 2 of the V1 build) ─
|
|
2
|
+
//
|
|
3
|
+
// The § Design 12 lowering says what a construct BECOMES on the compiled rule;
|
|
4
|
+
// this module says what it MEANS when `totem lint` runs. Two constructs need a
|
|
5
|
+
// runtime semantic the shipped engine does not have:
|
|
6
|
+
//
|
|
7
|
+
// § Design 7 — the two-array scope rule. A file is in scope iff it matches some
|
|
8
|
+
// `fileGlobs` entry AND matches no `excludeGlobs` entry. Exclusion
|
|
9
|
+
// is STRUCTURAL: entries are positive-form globs applied as
|
|
10
|
+
// exclusions, never `!`-negation (a parse error in the grammar).
|
|
11
|
+
// § Design 8 — the `requires:` two-pass. On a target match at locus L the rule
|
|
12
|
+
// FIRES iff `requires.pattern` does NOT match within the declared
|
|
13
|
+
// scope containing L.
|
|
14
|
+
//
|
|
15
|
+
// FREEZE / NON-REGRESSION BOUNDARY (binding): the legacy corpus (485 compiled
|
|
16
|
+
// rules, `.totem/compiled-rules.json`, frozen by `rule-compilation`) must keep
|
|
17
|
+
// matching BYTE-FOR-BYTE. Every function here is therefore gated on
|
|
18
|
+
// `isRecordPathRule`, and the mined-path byte-identity guard in
|
|
19
|
+
// `record-lower.test.ts` proves zero of the 485 legacy rules can reach a record
|
|
20
|
+
// branch. Nothing in this module runs `sanitizeFileGlobs`, and it never imports
|
|
21
|
+
// the frozen actuator.
|
|
22
|
+
//
|
|
23
|
+
// Purity: no IO, no clock, no module state. The `requires` evaluator takes the
|
|
24
|
+
// file text as a LAZY resolver so the caller owns the read (and so a `line`-scope
|
|
25
|
+
// requirement never forces one).
|
|
26
|
+
import * as path from 'node:path';
|
|
27
|
+
import { TotemParseError } from '../errors.js';
|
|
28
|
+
import { validateRegex } from '../regex-validation.js';
|
|
29
|
+
import { BoundedRegexCache, fileMatchesGlobs, matchesRecordGlob } from '../sys/glob.js';
|
|
30
|
+
/**
|
|
31
|
+
* The Prop 310 compiled homes, as a runtime-readable list. A rule carrying ANY
|
|
32
|
+
* of them came from the record path; a rule carrying NONE is legacy.
|
|
33
|
+
*
|
|
34
|
+
* DERIVED, never mirrored (Tenet 20): the discriminator is the presence of the
|
|
35
|
+
* § Design 12 homes themselves, not a separate `dialect:` marker field — a marker
|
|
36
|
+
* would be a second, drift-capable statement of what the fields already say, and
|
|
37
|
+
* § Design 4's key space is closed against inventing one.
|
|
38
|
+
*
|
|
39
|
+
* The derivation is SAFE because nothing but the record path can write these
|
|
40
|
+
* fields: they were added to `CompiledRuleSchema` by this slice, and the only
|
|
41
|
+
* other writer (the legacy lesson-compile actuator) is under the standing
|
|
42
|
+
* `rule-compilation` freeze and does not regenerate. The byte-identity guard
|
|
43
|
+
* asserts the empirical half — zero of the shipped 485 rules carries any of them.
|
|
44
|
+
*/
|
|
45
|
+
export const RECORD_COMPILED_HOME_KEYS = [
|
|
46
|
+
'excludeGlobs',
|
|
47
|
+
'requires',
|
|
48
|
+
'examples',
|
|
49
|
+
'language',
|
|
50
|
+
'verificationShadow',
|
|
51
|
+
'recoveryHint',
|
|
52
|
+
'curation',
|
|
53
|
+
];
|
|
54
|
+
/**
|
|
55
|
+
* True when a compiled rule came from the Prop 310 record path — i.e. when the
|
|
56
|
+
* § Design 7 dialect and its two-array scope rule apply to it. False for every
|
|
57
|
+
* legacy/mined rule, which keeps the shipped matcher unchanged.
|
|
58
|
+
*
|
|
59
|
+
* The test is `examples !== undefined` ALONE, deliberately narrower than "any
|
|
60
|
+
* § Design 12 home" (falsification round, 2026-08-21). `examples` is the EXACT
|
|
61
|
+
* discriminator, not merely a sufficient one:
|
|
62
|
+
* - § Design 5 makes it non-omittable and min-1, so EVERY record carries it;
|
|
63
|
+
* - `compileRuleRecord` emits it unconditionally, so every lowered rule has it;
|
|
64
|
+
* - it is the one home no other construct can be present without.
|
|
65
|
+
*
|
|
66
|
+
* The wider seven-key disjunction was demonstrated to be UNSAFE in the other
|
|
67
|
+
* direction: a legacy-shaped rule that ever gained just a `recoveryHint` — a
|
|
68
|
+
* plausible future addition to the mined path, and a purely cosmetic one — would
|
|
69
|
+
* silently flip from the shipped tree-wide matcher to the record dialect's
|
|
70
|
+
* root-only reading of the same `*.ts` glob, narrowing its scope with no signal.
|
|
71
|
+
* Keying on the field that MEANS "this is a record" removes that coupling
|
|
72
|
+
* between a diagnostic field and a matcher semantic.
|
|
73
|
+
*
|
|
74
|
+
* `RECORD_COMPILED_HOME_KEYS` remains the full home set for the corpus guard
|
|
75
|
+
* (which asserts the legacy manifest carries NONE of them — a stronger claim
|
|
76
|
+
* than this predicate needs, and the right one for a freeze check).
|
|
77
|
+
*/
|
|
78
|
+
export function isRecordPathRule(rule) {
|
|
79
|
+
return rule.examples !== undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The exemplar lines a rule's Stage-4 / doctor readers treat as "the authored bad
|
|
83
|
+
* example", single-homed (Tenet 20).
|
|
84
|
+
*
|
|
85
|
+
* Prop 310 slice 3 discharges the slice-2 pin: a record rule's authored preimages
|
|
86
|
+
* live in `examples[i].bad` — the § Design 10 EDITABLE home — and are NOT mirrored
|
|
87
|
+
* onto the legacy `badExample` field, so every reader of that field would otherwise
|
|
88
|
+
* read a record rule as having no bad example at all. Both shipped readers now ask
|
|
89
|
+
* this one function instead:
|
|
90
|
+
*
|
|
91
|
+
* - Stage 4 (`lineMatchesBadExample`) — a matched line equal to any of these
|
|
92
|
+
* classifies `in-scope-bad-example` rather than `candidate-debt`;
|
|
93
|
+
* - `totem doctor`'s `no-badExample` grandfathering reason — an empty result is
|
|
94
|
+
* exactly the old `!badExample || badExample.trim() === ''` test.
|
|
95
|
+
*
|
|
96
|
+
* BLANK LINES ARE DROPPED, which is what makes both of those byte-identical for
|
|
97
|
+
* legacy rules: Stage 4 already refuses to match an empty trimmed line, and a
|
|
98
|
+
* whitespace-only `badExample` must still count as absent for doctor.
|
|
99
|
+
*/
|
|
100
|
+
export function ruleBadExampleLines(rule) {
|
|
101
|
+
return exemplarLines(isRecordPathRule(rule) ? (rule.examples ?? []).map((e) => e.bad) : maybeBlock(rule.badExample));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The POSTIMAGE twin of `ruleBadExampleLines`, on the identical contract: record
|
|
105
|
+
* path → every `examples[i].good`, legacy → `goodExample`, blank lines dropped.
|
|
106
|
+
*
|
|
107
|
+
* `totem doctor`'s `no-goodExample` grandfathering reason reads it. Without the
|
|
108
|
+
* twin, a record rule cleared the `no-badExample` reason and then tripped
|
|
109
|
+
* `no-goodExample` on the very same absence — the record's exemplars live in
|
|
110
|
+
* `examples`, and § Design 10 makes that the editable home for BOTH sides of the
|
|
111
|
+
* pair, so a reader that single-homes one side and not the other reports half a
|
|
112
|
+
* truth. An empty result is exactly the old
|
|
113
|
+
* `!goodExample || goodExample.trim() === ''` test.
|
|
114
|
+
*/
|
|
115
|
+
export function ruleGoodExampleLines(rule) {
|
|
116
|
+
return exemplarLines(isRecordPathRule(rule)
|
|
117
|
+
? (rule.examples ?? []).map((e) => e.good)
|
|
118
|
+
: maybeBlock(rule.goodExample));
|
|
119
|
+
}
|
|
120
|
+
/** A present block as a one-element list, an absent one as none. */
|
|
121
|
+
function maybeBlock(block) {
|
|
122
|
+
return block === undefined ? [] : [block];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Split exemplar blocks into lines and DROP the blank ones — the shared half of
|
|
126
|
+
* both readers' contract. The blank-drop is what makes them byte-identical to the
|
|
127
|
+
* shipped behaviour for legacy rules: Stage 4 already refuses to match an empty
|
|
128
|
+
* trimmed line, and a whitespace-only exemplar field must still count as absent
|
|
129
|
+
* for doctor.
|
|
130
|
+
*/
|
|
131
|
+
function exemplarLines(blocks) {
|
|
132
|
+
return blocks.flatMap((block) => block.split(/\r?\n/)).filter((line) => line.trim().length > 0);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* § Design 12 — fail loud on a TORN record rule: one carrying a Prop 310
|
|
136
|
+
* compiled home but no `examples`.
|
|
137
|
+
*
|
|
138
|
+
* Such a rule cannot exist downstream of the lowering (`examples` is min-1 at
|
|
139
|
+
* parse and emitted unconditionally) nor in the frozen legacy corpus (which
|
|
140
|
+
* carries no home at all), so this only ever catches a hand-edited manifest or
|
|
141
|
+
* a bypassed producer. Left unchecked, `isRecordPathRule` would class it LEGACY
|
|
142
|
+
* and its `excludeGlobs` and `requires` would be dropped — a scope silently
|
|
143
|
+
* widened and a requirement silently unevaluated. The drop direction is toward
|
|
144
|
+
* flagging, so it is not fail-open, but § Design 12's ban on accepting a
|
|
145
|
+
* construct and dropping it is a ban on the SILENCE, not just on the unsafe
|
|
146
|
+
* direction. Fail loud instead.
|
|
147
|
+
*
|
|
148
|
+
* Called at dispatcher altitude, once per invocation — the same altitude as the
|
|
149
|
+
* tree-sitter `requires` backstop, and for the same reason: a per-file check
|
|
150
|
+
* would re-report the same defect on every file.
|
|
151
|
+
*/
|
|
152
|
+
export function assertNoTornRecordRules(rules) {
|
|
153
|
+
const torn = rules.find((rule) => rule.examples === undefined &&
|
|
154
|
+
RECORD_COMPILED_HOME_KEYS.some((key) => rule[key] !== undefined));
|
|
155
|
+
if (torn === undefined)
|
|
156
|
+
return;
|
|
157
|
+
const homes = RECORD_COMPILED_HOME_KEYS.filter((key) => torn[key] !== undefined);
|
|
158
|
+
throw new TotemParseError(`Rule ${torn.lessonHash} (${torn.lessonHeading}) carries Prop 310 record field(s) [${homes.join(', ')}] but no \`examples\`, so it can be classified neither as a record rule nor as a legacy one.`, 'Prop 310 § Design 5 makes `examples` non-omittable and the record lowering always emits it, so this manifest was hand-edited or written by a bypassed producer. Re-compile the rule record. Left unclassified, its `excludeGlobs` would be ignored and its `requires` never evaluated.');
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* § Design 7 — the two-array scope rule for a RECORD-path rule:
|
|
162
|
+
* `positiveMatch && !excludeMatch`, both arrays evaluated under the normative
|
|
163
|
+
* dialect (`matchesRecordGlob`).
|
|
164
|
+
*
|
|
165
|
+
* `fileGlobs` is min-1 at parse, so the empty-positive case cannot arise from a
|
|
166
|
+
* well-formed record; a hand-edited manifest that produced one would match
|
|
167
|
+
* NOTHING here rather than everything. That direction is deliberate — the
|
|
168
|
+
* shipped `fileMatchesGlobs` treats "no positive globs" as include-everything,
|
|
169
|
+
* which for a record-path rule would silently WIDEN a scope the grammar requires
|
|
170
|
+
* to be declared. A rule that fires nowhere is visible; one that fires everywhere
|
|
171
|
+
* is the fail-open class § Design 2 exists to kill.
|
|
172
|
+
*/
|
|
173
|
+
export function recordScopeMatchesFile(filePath, fileGlobs, excludeGlobs) {
|
|
174
|
+
const positives = fileGlobs ?? [];
|
|
175
|
+
if (!positives.some((glob) => matchesRecordGlob(filePath, glob)))
|
|
176
|
+
return false;
|
|
177
|
+
if (excludeGlobs === undefined)
|
|
178
|
+
return true;
|
|
179
|
+
return !excludeGlobs.some((glob) => matchesRecordGlob(filePath, glob));
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The ONE scope predicate the rule engine asks: does this rule apply to this
|
|
183
|
+
* file? Record-path rules get § Design 7's dialect + two-array rule; every other
|
|
184
|
+
* rule gets the shipped `fileMatchesGlobs` predicate verbatim, including its
|
|
185
|
+
* "unscoped rule applies to everything" behaviour.
|
|
186
|
+
*
|
|
187
|
+
* Single-homed on purpose: the engine had four separate copies of the
|
|
188
|
+
* `rule.fileGlobs && rule.fileGlobs.length > 0 ? fileMatchesGlobs(...) : true`
|
|
189
|
+
* expression, and four copies of a dialect fork is how a rule silently gets one
|
|
190
|
+
* scope at dispatch and a different one at the fail-loud guard.
|
|
191
|
+
*/
|
|
192
|
+
export function ruleAppliesToFile(rule, filePath) {
|
|
193
|
+
if (isRecordPathRule(rule)) {
|
|
194
|
+
return recordScopeMatchesFile(filePath, rule.fileGlobs, rule.excludeGlobs);
|
|
195
|
+
}
|
|
196
|
+
if (rule.fileGlobs && rule.fileGlobs.length > 0) {
|
|
197
|
+
return fileMatchesGlobs(filePath, rule.fileGlobs);
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Compiled `requires.pattern` instances, LRU-bounded and shared across every
|
|
203
|
+
* dispatcher. Keyed by pattern SOURCE, so two rules declaring the same
|
|
204
|
+
* requirement share one compilation and a manifest reload does not leak.
|
|
205
|
+
*/
|
|
206
|
+
const REQUIRES_PATTERN_CACHE_CAPACITY = 512;
|
|
207
|
+
const requiresPatternCache = new BoundedRegexCache(REQUIRES_PATTERN_CACHE_CAPACITY);
|
|
208
|
+
/**
|
|
209
|
+
* Path-containment check for the § Design 8 whole-file readers.
|
|
210
|
+
*
|
|
211
|
+
* The paths handed to these readers come out of a DIFF, which is
|
|
212
|
+
* attacker-shaped input on any path where a lint runs over untrusted contributed
|
|
213
|
+
* changes — a `../` prefix would otherwise read outside the repo. `path.relative`
|
|
214
|
+
* rather than `startsWith` on purpose: the string form admits the
|
|
215
|
+
* sibling-directory bypass (`/app-secrets` passing a `/app` prefix test), which
|
|
216
|
+
* is the same reasoning the shipped ast-path containment checks use.
|
|
217
|
+
*
|
|
218
|
+
* An out-of-root path is reported as UNREADABLE, not as an error: the requirement
|
|
219
|
+
* is then unmet and the rule FIRES, preserving the fail-toward-flagging direction
|
|
220
|
+
* every other read failure on this path takes.
|
|
221
|
+
*/
|
|
222
|
+
export function isInsideRoot(root, filePath) {
|
|
223
|
+
const resolvedRoot = path.resolve(root);
|
|
224
|
+
const relative = path.relative(resolvedRoot, path.resolve(resolvedRoot, filePath));
|
|
225
|
+
return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
226
|
+
}
|
|
227
|
+
/** Evaluate a compiled requirement against the declared § Design 8 scope. */
|
|
228
|
+
function matchesInScope(re, scope, text) {
|
|
229
|
+
if (scope === 'line')
|
|
230
|
+
return re.test(text.line);
|
|
231
|
+
const fileText = text.file();
|
|
232
|
+
if (fileText === null)
|
|
233
|
+
return false;
|
|
234
|
+
return re.test(fileText);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* § Design 8 — gate every `requires.pattern` in the loaded manifest through the
|
|
238
|
+
* SAME safe-regex2 check the compile path applies, before any of them is
|
|
239
|
+
* evaluated.
|
|
240
|
+
*
|
|
241
|
+
* The lowering already gates this, so a pattern reaching here unsafe means the
|
|
242
|
+
* manifest was hand-edited or written by a bypassed producer. Left ungated it is
|
|
243
|
+
* a live ReDoS surface, and a worse one than the target pattern's: at
|
|
244
|
+
* `scope: file` the requirement runs against WHOLE-FILE text, unbounded, on the
|
|
245
|
+
* pre-commit path. `validateRegex` is the same single-homed check the compile
|
|
246
|
+
* gate uses, so the two can never drift.
|
|
247
|
+
*
|
|
248
|
+
* Dispatcher altitude, once per invocation — the same reason as
|
|
249
|
+
* `assertNoTornRecordRules`: a per-match check would re-report one defect on
|
|
250
|
+
* every line of every file.
|
|
251
|
+
*/
|
|
252
|
+
export function assertRequiresPatternsSafe(rules) {
|
|
253
|
+
for (const rule of rules) {
|
|
254
|
+
if (rule.requires === undefined)
|
|
255
|
+
continue;
|
|
256
|
+
const validation = validateRegex(rule.requires.pattern);
|
|
257
|
+
if (validation.valid)
|
|
258
|
+
continue;
|
|
259
|
+
throw new TotemParseError(`Rule ${rule.lessonHash} (${rule.lessonHeading}) has an unusable \`requires.pattern\` (${validation.reason ?? 'invalid regex pattern'}) and cannot be evaluated (Prop 310 § Design 8).`, `The record-path compile gate runs this exact safe-regex2 check, so a pattern that fails it here means the manifest was hand-edited or written by a bypassed producer. Re-compile the rule record. At \`scope: file\` this pattern would run against whole-file text. Pattern: ${JSON.stringify(rule.requires.pattern)}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* § Design 8 — fail loud on `ast-grep` + `requires.scope: line`.
|
|
264
|
+
*
|
|
265
|
+
* The lowering REJECTS this combination (an ast-grep match is a span, and
|
|
266
|
+
* reading it as its start line makes the verdict depend on source formatting),
|
|
267
|
+
* but a hand-edited manifest can still carry it, and the ast dispatcher would
|
|
268
|
+
* silently evaluate `match.lineText` — shipping exactly the formatting-dependent
|
|
269
|
+
* semantic the ruling refused. Mirrors the tree-sitter `requires` backstop:
|
|
270
|
+
* unreachable from any compiled record, unreachable from the legacy corpus,
|
|
271
|
+
* live only against a torn manifest.
|
|
272
|
+
*/
|
|
273
|
+
export function assertNoAstGrepLineScope(rules) {
|
|
274
|
+
const offender = rules.find((rule) => rule.engine === 'ast-grep' && rule.requires?.scope === 'line');
|
|
275
|
+
if (offender === undefined)
|
|
276
|
+
return;
|
|
277
|
+
throw new TotemParseError(`Rule ${offender.lessonHash} (${offender.lessonHeading}) carries \`requires.scope: line\` on the \`ast-grep\` engine, a combination the record lowering rejects.`, "Prop 310 § Design 8: an ast-grep match is a SPAN, and reading it as its start line makes the verdict depend on source formatting rather than on the rule. No rule record can compile to this shape, so the manifest was hand-edited or written by a bypassed producer. Use `scope: file`, move the requirement into the payload's own `not:`/`has:` combinators, or wait for the reserved `block` unit.");
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* § Design 8 — pass two. Returns TRUE when the required context is PRESENT
|
|
281
|
+
* within the declared scope containing L, i.e. when the rule must stay SILENT.
|
|
282
|
+
* The rule fires iff the target matched AND this returns false.
|
|
283
|
+
*
|
|
284
|
+
* `requires.pattern` is a regex evaluated TEXTUALLY, independent of the target
|
|
285
|
+
* engine — "the requirement is a context check, not a second matcher" — so an
|
|
286
|
+
* ast-grep rule's requirement is still a plain regex over the scope text.
|
|
287
|
+
*
|
|
288
|
+
* UNREADABLE FILE (`scope: file` only) ⇒ context ABSENT ⇒ the rule FIRES. The
|
|
289
|
+
* safe direction for a lint gate is toward flagging: a false positive is visible
|
|
290
|
+
* and disputable, a suppressed real violation is silent. This mirrors the shipped
|
|
291
|
+
* Rust test-span exemption, whose read failure likewise yields no exemption
|
|
292
|
+
* (`rule-engine.ts`: "the exemption fails toward flagging, never toward
|
|
293
|
+
* suppression").
|
|
294
|
+
*
|
|
295
|
+
* A `requires.pattern` that will not compile means the compile-stage gate was
|
|
296
|
+
* bypassed or the manifest was hand-edited. Fail LOUD — the same treatment
|
|
297
|
+
* `applyRulesToAdditions` gives an uncompilable `rule.pattern`; silently treating
|
|
298
|
+
* it as "context absent" would turn a broken rule into a firing one.
|
|
299
|
+
*/
|
|
300
|
+
export function requiresContextPresent(rule, requires, text) {
|
|
301
|
+
// Compiled once per distinct pattern, not once per MATCH: this runs on every
|
|
302
|
+
// target hit of every requires-carrying rule, and at `scope: file` against
|
|
303
|
+
// whole-file text. The pattern set is manifest-bounded, and the LRU bound keeps
|
|
304
|
+
// a long-lived process (the MCP server, a watch loop) from growing a map keyed
|
|
305
|
+
// by rule content.
|
|
306
|
+
let re = requiresPatternCache.get(requires.pattern);
|
|
307
|
+
if (re !== undefined) {
|
|
308
|
+
return matchesInScope(re, requires.scope, text);
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
re = new RegExp(requires.pattern);
|
|
312
|
+
requiresPatternCache.set(requires.pattern, re);
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
throw new TotemParseError(`Rule ${rule.lessonHash} has an invalid \`requires.pattern\` and cannot be evaluated (Prop 310 § Design 8).`, `The record-path compile gate validates this pattern with the same safe-regex2 check it applies to the target, so an invalid one here means the manifest was hand-edited or written by a bypassed producer. Re-compile the rule record. Pattern: ${JSON.stringify(requires.pattern)}`, err);
|
|
316
|
+
}
|
|
317
|
+
return matchesInScope(re, requires.scope, text);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* § Design 8 applied to one target match: TRUE when the rule must stay silent at
|
|
321
|
+
* this locus because its required context is present. Always false for a rule
|
|
322
|
+
* carrying no `requires` block — which is every legacy rule, so the shipped
|
|
323
|
+
* evaluation path is untouched by construction.
|
|
324
|
+
*/
|
|
325
|
+
export function requiresSuppressesMatch(rule, text) {
|
|
326
|
+
if (rule.requires === undefined)
|
|
327
|
+
return false;
|
|
328
|
+
return requiresContextPresent(rule, rule.requires, text);
|
|
329
|
+
}
|
|
330
|
+
//# sourceMappingURL=record-runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-runtime.js","sourceRoot":"","sources":["../../src/spine/record-runtime.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,EAAE;AACF,+EAA+E;AAC/E,+EAA+E;AAC/E,qDAAqD;AACrD,EAAE;AACF,kFAAkF;AAClF,kFAAkF;AAClF,2EAA2E;AAC3E,gFAAgF;AAChF,iFAAiF;AACjF,iFAAiF;AACjF,qCAAqC;AACrC,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,oEAAoE;AACpE,gEAAgE;AAChE,gFAAgF;AAChF,gFAAgF;AAChF,uBAAuB;AACvB,EAAE;AACF,+EAA+E;AAC/E,kFAAkF;AAClF,iCAAiC;AAEjC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAGlC,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExF;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACvC,cAAc;IACd,UAAU;IACV,UAAU;IACV,UAAU;IACV,oBAAoB;IACpB,cAAc;IACd,UAAU;CACwC,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAoC;IACnE,OAAO,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC;AACrC,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAmD;IACrF,OAAO,aAAa,CAClB,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,CAC/F,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAoD;IAEpD,OAAO,aAAa,CAClB,gBAAgB,CAAC,IAAI,CAAC;QACpB,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QAC1C,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,CACjC,CAAC;AACJ,CAAC;AAED,oEAAoE;AACpE,SAAS,UAAU,CAAC,KAAyB;IAC3C,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;;GAMG;AACH,SAAS,aAAa,CAAC,MAAyB;IAC9C,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAClG,CAAC;AAUD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAA8B;IACpE,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CACrB,CAAC,IAAI,EAAE,EAAE,CACP,IAAI,CAAC,QAAQ,KAAK,SAAS;QAC3B,yBAAyB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC,CACnE,CAAC;IACF,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO;IAC/B,MAAM,KAAK,GAAG,yBAAyB,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC,CAAC;IACjF,MAAM,IAAI,eAAe,CACvB,QAAQ,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC,aAAa,uCAAuC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,8FAA8F,EACnM,wRAAwR,CACzR,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,sBAAsB,CACpC,QAAgB,EAChB,SAAwC,EACxC,YAA2C;IAE3C,MAAM,SAAS,GAAG,SAAS,IAAI,EAAE,CAAC;IAClC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/E,IAAI,YAAY,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC5C,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;AACzE,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAqB,EAAE,QAAgB;IACvE,IAAI,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3B,OAAO,sBAAsB,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IAC7E,CAAC;IACD,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChD,OAAO,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAiBD;;;;GAIG;AACH,MAAM,+BAA+B,GAAG,GAAG,CAAC;AAC5C,MAAM,oBAAoB,GAAG,IAAI,iBAAiB,CAAC,+BAA+B,CAAC,CAAC;AAEpF;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,QAAgB;IACzD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAC;IACnF,OAAO,QAAQ,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;AACrF,CAAC;AAED,6EAA6E;AAC7E,SAAS,cAAc,CAAC,EAAU,EAAE,KAAsB,EAAE,IAAuB;IACjF,IAAI,KAAK,KAAK,MAAM;QAAE,OAAO,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACpC,OAAO,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,0BAA0B,CAAC,KAA8B;IACvE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;YAAE,SAAS;QAC1C,MAAM,UAAU,GAAG,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACxD,IAAI,UAAU,CAAC,KAAK;YAAE,SAAS;QAC/B,MAAM,IAAI,eAAe,CACvB,QAAQ,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC,aAAa,2CAA2C,UAAU,CAAC,MAAM,IAAI,uBAAuB,kDAAkD,EACvL,iRAAiR,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CACzT,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAA8B;IACrE,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CACzB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,UAAU,IAAI,IAAI,CAAC,QAAQ,EAAE,KAAK,KAAK,MAAM,CACxE,CAAC;IACF,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO;IACnC,MAAM,IAAI,eAAe,CACvB,QAAQ,QAAQ,CAAC,UAAU,KAAK,QAAQ,CAAC,aAAa,2GAA2G,EACjK,yYAAyY,CAC1Y,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,sBAAsB,CACpC,IAAkB,EAClB,QAA+C,EAC/C,IAAuB;IAEvB,6EAA6E;IAC7E,2EAA2E;IAC3E,gFAAgF;IAChF,+EAA+E;IAC/E,mBAAmB;IACnB,IAAI,EAAE,GAAG,oBAAoB,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACpD,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;QACrB,OAAO,cAAc,CAAC,EAAE,EAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAClD,CAAC;IACD,IAAI,CAAC;QACH,EAAE,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAClC,oBAAoB,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACjD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,eAAe,CACvB,QAAQ,IAAI,CAAC,UAAU,qFAAqF,EAC5G,mPAAmP,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,EACrR,GAAG,CACJ,CAAC;IACJ,CAAC;IACD,OAAO,cAAc,CAAC,EAAE,EAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AAClD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAAkB,EAAE,IAAuB;IACjF,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAC9C,OAAO,sBAAsB,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record-runtime.test.d.ts","sourceRoot":"","sources":["../../src/spine/record-runtime.test.ts"],"names":[],"mappings":""}
|