@try-works/dsh-recursive-mode 0.4.9 → 0.6.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/README.md +21 -8
- package/lib/config.d.ts +14 -0
- package/lib/enforcement.d.ts +118 -0
- package/lib/guard-log.d.ts +7 -0
- package/lib/index.js +568 -133
- package/lib/memory.d.ts +35 -2
- package/lib/phase-rules.d.ts +102 -0
- package/lib/policy-globs.d.ts +24 -0
- package/lib/recursive_ask.tool.d.ts +37 -0
- package/lib/runtime.d.ts +1 -1
- package/lib/training.d.ts +240 -2
- package/package.json +1 -1
- package/references/artifact-template.md +26 -61
- package/references/bodies/claude.md +1 -1
- package/references/bodies/copilot.md +1 -1
- package/references/bodies/cursorrules.md +4 -2
- package/references/bodies/memory-router.md +1 -1
- package/references/bodies/recursive-agents-router.md +4 -3
- package/references/bootstrap/RECURSIVE.md +21 -30
- package/scripts/test-recursive-mode-smoke.ts +11 -1
- package/src/bootstrap.ts +30 -14
- package/src/config.ts +11 -4
- package/src/enforcement.ts +202 -15
- package/src/guard-log.ts +7 -0
- package/src/index.ts +795 -700
- package/src/memory.ts +50 -9
- package/src/phase-rules.ts +162 -1
- package/src/policy-globs.ts +64 -8
- package/src/recursive_ask.tool.ts +48 -0
- package/src/recursive_lock.tool.ts +8 -2
- package/src/runtime.ts +7 -4
- package/src/training.ts +634 -6
package/README.md
CHANGED
|
@@ -289,10 +289,9 @@ Two conventions matter more than they look:
|
|
|
289
289
|
repoRoot)` resolves the workspace the *session* is in. A plugin that used `process.cwd()` would silently lint the
|
|
290
290
|
wrong repository when the host checkout and the workspace differ.
|
|
291
291
|
|
|
292
|
-
**The memory plane is read
|
|
293
|
-
|
|
294
|
-
root
|
|
295
|
-
which is why the distinction is written down here.
|
|
292
|
+
**The memory plane is read from the plane the scaffold creates, not from beside it.** `loadMemoryIndex` resolves each kind under `<controlPlaneRoot>/.recursive/memory/<kind>/` — the directory `bootstrap.ts` scaffolds, `ts-lint.ts` lints, and the review bundle already read — and falls back to `<controlPlaneRoot>/memory/<kind>/` only when that kind has no readable shard there. That is the same **prefer-then-fall-back, never merge** rule `readFeedback` follows for its own moved sidecar: the earlier location is consulted only when the current one yields nothing, because two snapshots of one shard added together would count a shard twice.
|
|
293
|
+
|
|
294
|
+
Before that fallback existed the loader joined `memory/` straight onto the root it was handed, so it read `<dir>/memory/domains` — **a directory no real workspace has** — while the plane sat at `<dir>/.recursive/memory/domains`. Measured live: a workspace with the plane scaffolded, `memory/training/` included, and no `<dir>/memory/` at all, where `selectMemory` reported *"the memory plane is empty"* on every phase of every run. Passing `.recursive` itself as the root still resolves, through the second entry.
|
|
296
295
|
|
|
297
296
|
---
|
|
298
297
|
|
|
@@ -562,11 +561,12 @@ inputs are visible.
|
|
|
562
561
|
|
|
563
562
|
```mermaid
|
|
564
563
|
flowchart LR
|
|
565
|
-
subgraph sources["memory/<kind>/*.md"]
|
|
564
|
+
subgraph sources[".recursive/memory/<kind>/*.md"]
|
|
566
565
|
D1["domains/"]
|
|
567
566
|
D2["patterns/"]
|
|
568
|
-
D3["
|
|
569
|
-
D4["
|
|
567
|
+
D3["episodes/"]
|
|
568
|
+
D4["training/"]
|
|
569
|
+
D5["skills/"]
|
|
570
570
|
end
|
|
571
571
|
|
|
572
572
|
Q["query:<br/>the run's own<br/>00-requirements.md<br/>(first 4000 chars)"] --> SEL
|
|
@@ -578,8 +578,9 @@ flowchart LR
|
|
|
578
578
|
D2 --> IDX
|
|
579
579
|
D3 --> IDX
|
|
580
580
|
D4 --> IDX
|
|
581
|
+
D5 --> IDX
|
|
581
582
|
|
|
582
|
-
SEL --> OUT["top N shards →<br/>the review bundle"]
|
|
583
|
+
SEL --> OUT["top N shards →<br/>the phase payload<br/>and the review bundle"]
|
|
583
584
|
SEL --> REC["recordInjection()<br/>memory-injections.json"]
|
|
584
585
|
REC --> SET["settleInjections() at closeout<br/>over the LOCKED phases only"]
|
|
585
586
|
SET --> FB
|
|
@@ -588,6 +589,12 @@ flowchart LR
|
|
|
588
589
|
class Q,F,P,FB input
|
|
589
590
|
```
|
|
590
591
|
|
|
592
|
+
**`MEMORY_KINDS` is the list of kinds that are auto-loaded, and it is exactly those five.** `incidents/` and
|
|
593
|
+
`archive/` are stored and documented by the router but are *not* auto-loaded — `archive/` is historical by the
|
|
594
|
+
router's own definition, and adding `incidents/` is a ranking decision rather than a typo fix. `training/` **is**
|
|
595
|
+
in the list because it is the kind the phase-8 trigger writes: with it absent, the shards training produced were
|
|
596
|
+
written and registered in `MEMORY.md` while the loader that was supposed to score them could not see the kind.
|
|
597
|
+
|
|
591
598
|
**Scoring is additive and inspectable.** A shard scores for matching the query, for overlapping the changed
|
|
592
599
|
paths (`MEMORY_PATH_MATCH_WEIGHT = 3`), for applying to the current phase (`MEMORY_PHASE_MATCH_WEIGHT = 2`), and
|
|
593
600
|
for its feedback history (`feedbackBonus`, clamped to ±1). `explainMemorySelection` returns the **components** per
|
|
@@ -689,6 +696,12 @@ Which closes the loop the [memory plane](#9-how-it-works-the-memory-plane) opene
|
|
|
689
696
|
closeout, and `feedbackBonus` nudges the next selection. The plugin's prompts and its context are meant to
|
|
690
697
|
improve from its own recorded outcomes rather than from someone's recollection of them.
|
|
691
698
|
|
|
699
|
+
**And the loop is only closed when the reader can see every kind the writer produces.** The trigger writes one
|
|
700
|
+
shard per subsystem into `memory/domains/` *and* one per task type into `memory/training/<task-type>.md`, and the
|
|
701
|
+
second of those is the reason `training` is in `MEMORY_KINDS` (§9): a writer whose output the reader cannot reach
|
|
702
|
+
is the loop being described rather than closed. `tests/training-trigger.spec.ts` asserts the round trip end to
|
|
703
|
+
end — trigger writes, loader finds the shard it wrote — because either half can look correct alone.
|
|
704
|
+
|
|
692
705
|
**Design and status:** [TRAINING.md](TRAINING.md) is the design — including the phases P1–P5, each with a live
|
|
693
706
|
acceptance, and the measured comparison with the reference implementation it deliberately does **not** copy.
|
|
694
707
|
Its non-goals are as load-bearing as its goals: this is not a fine-tuning pipeline, and it does not ship a model
|
package/lib/config.d.ts
CHANGED
|
@@ -48,6 +48,13 @@ export interface RecursiveModeConfig {
|
|
|
48
48
|
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
49
49
|
shellOnly: z<boolean, boolean, "defined">;
|
|
50
50
|
repoRoot: z<string, string, "plain">;
|
|
51
|
+
/**
|
|
52
|
+
* ⚠ THE THREE MODE DEFAULTS READ `DEFAULT_ENFORCEMENT_MODE` FROM `enforcement.ts`, and
|
|
53
|
+
* that is deliberate: the schema default and the runtime default are the SAME value, and
|
|
54
|
+
* two literals here would be two defaults. A caller that omits the section gets
|
|
55
|
+
* `DEFAULT_ENFORCEMENT` from the runtime; a caller that supplies a partial section gets
|
|
56
|
+
* the resolver's fill. Both must be the enforcing posture — see the const for why.
|
|
57
|
+
*/
|
|
51
58
|
enforcement: z<Schemastery.ObjectS<NoInfer<{
|
|
52
59
|
preStep: z<"strict" | "advisory", "strict" | "advisory", "defined">;
|
|
53
60
|
toolGuards: z<"strict" | "advisory", "strict" | "advisory", "defined">;
|
|
@@ -125,6 +132,13 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
125
132
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
126
133
|
shellOnly: z<boolean, boolean, "defined">;
|
|
127
134
|
repoRoot: z<string, string, "plain">;
|
|
135
|
+
/**
|
|
136
|
+
* ⚠ THE THREE MODE DEFAULTS READ `DEFAULT_ENFORCEMENT_MODE` FROM `enforcement.ts`, and
|
|
137
|
+
* that is deliberate: the schema default and the runtime default are the SAME value, and
|
|
138
|
+
* two literals here would be two defaults. A caller that omits the section gets
|
|
139
|
+
* `DEFAULT_ENFORCEMENT` from the runtime; a caller that supplies a partial section gets
|
|
140
|
+
* the resolver's fill. Both must be the enforcing posture — see the const for why.
|
|
141
|
+
*/
|
|
128
142
|
enforcement: z<Schemastery.ObjectS<NoInfer<{
|
|
129
143
|
preStep: z<"strict" | "advisory", "strict" | "advisory", "defined">;
|
|
130
144
|
toolGuards: z<"strict" | "advisory", "strict" | "advisory", "defined">;
|
package/lib/enforcement.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { builtInToolPolicyDefault, type ToolPolicy } from './policy-globs.ts';
|
|
2
|
+
import { type GateBlockAsk } from './recursive_ask.tool.ts';
|
|
2
3
|
/**
|
|
3
4
|
* The BUILT-IN default rule list (T16) is defined in `src/policy-globs.ts`,
|
|
4
5
|
* beside the evaluator and the loader that use it as the ABSENT-file fallback,
|
|
@@ -40,6 +41,28 @@ export interface EnforcementConfig {
|
|
|
40
41
|
/** T28: the caps. Always present, so a caller never has to guess a default. */
|
|
41
42
|
budgets: BudgetConfig;
|
|
42
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* THE DEFAULT POSTURE: STRICT, on all three gates — and this const is the ONE literal.
|
|
46
|
+
*
|
|
47
|
+
* The owner's rule is *only one phase may be active at a time, and the phases should be
|
|
48
|
+
* sequential and the active phase must be locked before proceeding to next phase*. In
|
|
49
|
+
* `advisory` that rule is only WARNED about, and a live run showed what that costs: the
|
|
50
|
+
* run ignored the lock chain for over an hour, wrote phase 8 before phase 1.5 and locked
|
|
51
|
+
* nothing (twelve DRAFT artifacts, one operations entry). Strict was previously unsafe as
|
|
52
|
+
* a default because it also refused the run's OWN artifacts — a false positive. That was
|
|
53
|
+
* fixed, and `tests/strict-run-tree.spec.ts` now walks all twelve phases asserting the
|
|
54
|
+
* active artifact stays writable while a later one is refused. Strict therefore refuses
|
|
55
|
+
* exactly the ordering violations it is meant to refuse, so the default is the enforcing
|
|
56
|
+
* posture rather than a warning nobody has to act on.
|
|
57
|
+
*
|
|
58
|
+
* ⚠ WHY IT IS A NAMED CONST AND NOT THREE LITERALS. A default restated per site is this
|
|
59
|
+
* project's recurring failure: the same value exists in the Config schema, in
|
|
60
|
+
* `DEFAULT_ENFORCEMENT`, in an omitted config section, and in the parameter defaults of
|
|
61
|
+
* the helpers below, and moving only some of them leaves a caller that "still gets
|
|
62
|
+
* advisory". Every one of those sites now reads THIS const, so a revert is a one-line
|
|
63
|
+
* change and nothing can drift from it.
|
|
64
|
+
*/
|
|
65
|
+
export declare const DEFAULT_ENFORCEMENT_MODE: EnforcementMode;
|
|
43
66
|
/**
|
|
44
67
|
* Validate the enforcement config shape (unknown keys fail at plugin load).
|
|
45
68
|
*
|
|
@@ -49,6 +72,12 @@ export interface EnforcementConfig {
|
|
|
49
72
|
* A config error is loud at load, not mysterious later.
|
|
50
73
|
*/
|
|
51
74
|
export declare function resolveEnforcementConfig(config: unknown): EnforcementConfig;
|
|
75
|
+
/**
|
|
76
|
+
* The runtime default: what a caller gets when it supplies no `enforcement` section at all
|
|
77
|
+
* (a profile mounting this plugin with no config, e.g. `preset/recursive.patch.yml`). It is
|
|
78
|
+
* `DEFAULT_ENFORCEMENT_MODE` per gate, so this object and the resolver cannot disagree —
|
|
79
|
+
* see that const for WHY the default is strict.
|
|
80
|
+
*/
|
|
52
81
|
export declare const DEFAULT_ENFORCEMENT: EnforcementConfig;
|
|
53
82
|
/**
|
|
54
83
|
* T15: the rule that produced a guard decision (a machine-readable reason for
|
|
@@ -77,22 +106,39 @@ export interface GuardTransition {
|
|
|
77
106
|
* optional — `coerceAskToDecision` is asserted with `toEqual({ kind: ... })`
|
|
78
107
|
* (an EXACT match) in tests/enforcement.spec.ts, so the coercion path may never
|
|
79
108
|
* grow extra keys. `evaluateToolGuard` itself always sets `rule`.
|
|
109
|
+
*
|
|
110
|
+
* ⚠ FU-7: `ask` IS OPTIONAL AND ADDITIVE TOO, for the same reason and one more. A refusal that a
|
|
111
|
+
* PERSON has to resolve carries the gate-block decision alongside its sentence (see `verdictFor`),
|
|
112
|
+
* and the payload is built by `buildGateBlockAsk` — the SAME builder the lock tool uses, so the two
|
|
113
|
+
* refusals cannot offer different options. It is absent on every decision that is not a lock-order
|
|
114
|
+
* refusal decided from real blockers, which is why every reader must treat it as optional.
|
|
115
|
+
*
|
|
116
|
+
* ⚠ ISSUE 2 (b): `runId` IS THE RUN THE GUARD RESOLVED AND READ — optional and additive for the same
|
|
117
|
+
* reason. `evaluateToolGuard` sets it on EVERY decision, allow included, and `index.ts` logs it instead of
|
|
118
|
+
* the filesystem's active run: a record whose `runId` came from one resolution while the rule read another
|
|
119
|
+
* is exactly the two-answers-in-one-payload defect this pairs with. It stays OPTIONAL so a hand-built
|
|
120
|
+
* decision (and `coerceAskToDecision`'s key-frozen input/output) is unaffected; a reader falls back to the
|
|
121
|
+
* active run when it is absent, which is the pre-existing behaviour.
|
|
80
122
|
*/
|
|
81
123
|
export type ToolGuardDecision = {
|
|
82
124
|
kind: 'allow';
|
|
83
125
|
warn?: string;
|
|
84
126
|
rule?: GuardRule;
|
|
85
127
|
transition?: GuardTransition;
|
|
128
|
+
runId?: string;
|
|
86
129
|
} | {
|
|
87
130
|
kind: 'deny';
|
|
88
131
|
reason: string;
|
|
89
132
|
rule?: GuardRule;
|
|
90
133
|
transition?: GuardTransition;
|
|
134
|
+
ask?: GateBlockAsk;
|
|
135
|
+
runId?: string;
|
|
91
136
|
} | {
|
|
92
137
|
kind: 'ask';
|
|
93
138
|
reason?: string;
|
|
94
139
|
rule?: GuardRule;
|
|
95
140
|
transition?: GuardTransition;
|
|
141
|
+
runId?: string;
|
|
96
142
|
};
|
|
97
143
|
export interface ToolExecLike {
|
|
98
144
|
name: string;
|
|
@@ -122,12 +168,84 @@ export declare function resolveToolPolicyForGuard(worktreeRoot: string, runId: s
|
|
|
122
168
|
* needs, because a cached phase would apply yesterday's baseline to today's lock.
|
|
123
169
|
*/
|
|
124
170
|
export declare function currentPhaseArtifact(worktreeRoot: string, runId: string): string;
|
|
171
|
+
/**
|
|
172
|
+
* ISSUE 2 (a) — THE RUN A GUARD CALL IS ABOUT, and the one whose tree it may read.
|
|
173
|
+
*
|
|
174
|
+
* THE DEFECT THIS ANSWERS, measured before the fix: `recursive_lock {runId: 'run-b', artifact:
|
|
175
|
+
* '01-as-is.md'}` was REFUSED with `monotonic lock-order: … 00-requirements.md (DRAFT)` — run-A's blocker —
|
|
176
|
+
* while `run-b` had `00-requirements.md` LOCKED and `01-as-is.md` DRAFT, so locking it in run-b was LEGAL.
|
|
177
|
+
* The guard resolved the run from the FILESYSTEM (`resolveRunDir`, i.e. the active/newest run) while the
|
|
178
|
+
* tool resolves it from `args.runId`, so the guard judged a DIFFERENT RUN than the call was about. Under
|
|
179
|
+
* `advisory` the deny was coerced to an allow-with-warning and the tool refused on its own terms, which is
|
|
180
|
+
* why the strict default is what made it bite.
|
|
181
|
+
*
|
|
182
|
+
* SO THE RULE IS: for a LOCK call that NAMES a run, the guard judges THAT RUN. It is the same choice the
|
|
183
|
+
* tool makes, so the two layers cannot disagree about which tree the ordering rule is a property of. A
|
|
184
|
+
* caller that names nothing (every real `write`, and a lock that relies on the active run) is unaffected:
|
|
185
|
+
* the active run still governs, which is what the write-side rules rely on.
|
|
186
|
+
*
|
|
187
|
+
* ⚠ THIS IS SCOPED TO THE LOCK TOOLS DELIBERATELY, and the scope is per rule, not per convenience:
|
|
188
|
+
*
|
|
189
|
+
* - `lock-order` (`recursive_lock*`) — the caller's run WINS. The tool acts on `args.runId`, and the
|
|
190
|
+
* rule is about THAT run's prerequisites, so the guard must not answer for another run. This is the
|
|
191
|
+
* measured defect.
|
|
192
|
+
* - `locked-write` (the write-tool family) — NOT APPLICABLE, by construction: the rule resolves no run
|
|
193
|
+
* at all. It reads the target file's own `Status:` through the path the caller named, so there is no
|
|
194
|
+
* run to prefer and nothing could disagree.
|
|
195
|
+
* - `phase-order` (the write-tool family) — the ACTIVE run KEEPS WINNING, and this function does not
|
|
196
|
+
* touch it. Two reasons, both deliberate: (1) a `write` call carries no run id — no write tool declares
|
|
197
|
+
* one — so consulting `args.runId` here would hand a caller a way to ESCAPE the active run's ordering
|
|
198
|
+
* by naming some other run in an argument the tool ignores; and (2) the rule's declared scope is the
|
|
199
|
+
* run being worked in (it abstains for another run's tree, documented in `phaseOrderRule`), and moving
|
|
200
|
+
* that scope would be a new refusal, not a consistency fix.
|
|
201
|
+
*
|
|
202
|
+
* ⚠ A CALLER-SUPPLIED ID IS A NAME, NEVER A PATH, and it is validated before it can point the guard at
|
|
203
|
+
* anything: the id is trimmed the way `recursive_lock` trims it, then put through `runIdProblem` — the
|
|
204
|
+
* SAME gate the run-id-shaped tools use, which refuses separators, drive specifiers, `..`, a colon, a
|
|
205
|
+
* leading/trailing dot and an over-long name — and finally the resolved directory must sit UNDER this
|
|
206
|
+
* worktree's `<root>/.recursive/run`, the containment rule `runtime.ts` applies to a run directory.
|
|
207
|
+
*
|
|
208
|
+
* An id that fails any of those is NOT USED: the guard falls back to the active run, exactly as it behaved
|
|
209
|
+
* before this change. Falling back (rather than denying) is deliberate: an unusable id is a caller mistake
|
|
210
|
+
* the tool itself refuses (`BAD_RUN_ID` / `Artifact not found`), and inventing a new guard refusal for it
|
|
211
|
+
* would be a second, competing answer to a question `runIdProblem` already owns.
|
|
212
|
+
*
|
|
213
|
+
* A usable id does NOT have to name an EXISTING run: a run with no tree has no unlocked prerequisites, so
|
|
214
|
+
* the ordering rule abstains and the LOCK TOOL still refuses the lock (it checks the artifact exists before
|
|
215
|
+
* anything else). Requiring existence would instead re-introduce the defect in its ugliest form — a refusal
|
|
216
|
+
* built from ANOTHER run's blockers.
|
|
217
|
+
*/
|
|
218
|
+
export declare function resolveGuardRunId(name: string, args: Record<string, unknown>, worktreeRoot: string, activeRunId: string): string;
|
|
219
|
+
/**
|
|
220
|
+
* `mode` is the gate's configured posture. Its parameter default FOLLOWS the config
|
|
221
|
+
* default by REFERENCE (`DEFAULT_ENFORCEMENT.toolGuards`) rather than repeating the
|
|
222
|
+
* literal: a bare call is "the caller had no mode to hand", and the answer to that must
|
|
223
|
+
* be the same posture the config would have produced. Two literals are two defaults, and
|
|
224
|
+
* a helper left on the old `advisory` literal while the config moved to `strict` is
|
|
225
|
+
* exactly the twin-default hole this change closes — a caller that forgot the argument
|
|
226
|
+
* would silently get the permissive branch, which no config could then undo. Every
|
|
227
|
+
* production call site passes the mode explicitly (`index.ts` `runToolGuard`,
|
|
228
|
+
* `runtime.ts` `guardTool`, the preview tool); this default serves bare callers, and a
|
|
229
|
+
* bare caller must not be the one place enforcement quietly turns itself off.
|
|
230
|
+
*/
|
|
125
231
|
export declare function evaluateToolGuard(exec: ToolExecLike, worktreeRoot: string, activeRunId: string, mode?: EnforcementMode): ToolGuardDecision;
|
|
126
232
|
/**
|
|
127
233
|
* T6 (approval ask→policy bridge): an `ask` decision must never be a silent
|
|
128
234
|
* allow. Under `strict` it coerces to `deny`; under `advisory` it stays `allow`
|
|
129
235
|
* but flags a `warn` so the caller never lets it through unlogged. Non-ask
|
|
130
236
|
* decisions pass through unchanged.
|
|
237
|
+
*
|
|
238
|
+
* ⚠ THE `mode` DEFAULT IS DELIBERATE, and it is NOT a neutral fallback — there is no
|
|
239
|
+
* neutral branch here. The domain is two postures, one of which ALLOWS the call, so
|
|
240
|
+
* "unspecified" has to be resolved rather than left open, and this codebase's rule for an
|
|
241
|
+
* undecidable path is to fail CLOSED (`index.ts`: *"we could not decide" is not
|
|
242
|
+
* permission*). It therefore FOLLOWS the config default by REFERENCE
|
|
243
|
+
* (`DEFAULT_ENFORCEMENT.toolGuards`), for the same reason as `evaluateToolGuard`'s: an
|
|
244
|
+
* `advisory` literal here would be a second, hidden copy of the old default inside the
|
|
245
|
+
* very module this change moves, and a future caller that omitted the argument would
|
|
246
|
+
* re-open the permissive path with no config able to close it. The production call site
|
|
247
|
+
* (`index.ts` `runToolGuard`) always passes the configured mode, so this changes no live
|
|
248
|
+
* behaviour — it removes the last place where "we were not told" meant "allow".
|
|
131
249
|
*/
|
|
132
250
|
export declare function coerceAskToDecision(decision: ToolGuardDecision, mode?: EnforcementMode): ToolGuardDecision;
|
|
133
251
|
/**
|
package/lib/guard-log.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { GuardRule } from './enforcement.ts';
|
|
2
|
+
import type { GateBlockAsk } from './recursive_ask.tool.ts';
|
|
2
3
|
/**
|
|
3
4
|
* One logged guard decision (the JSONL record shape the board/tests read).
|
|
4
5
|
* `rule` is always set (the guard's own machine-readable reason for the
|
|
5
6
|
* verdict); `transition` is present whenever the transition gate was consulted.
|
|
7
|
+
*
|
|
8
|
+
* FU-7: a REFUSAL that a person has to resolve also carries `ask` — the gate-block decision, in the
|
|
9
|
+
* same shape `recursive_lock` attaches to its own refusal. It is recorded because the log is where
|
|
10
|
+
* "why was this lock refused?" is answered, and the options are the other half of that answer; a
|
|
11
|
+
* caller (or a board) reading the trace can act on the refusal without parsing the sentence.
|
|
6
12
|
*/
|
|
7
13
|
export interface GuardDecisionRecord {
|
|
8
14
|
at: string;
|
|
@@ -15,6 +21,7 @@ export interface GuardDecisionRecord {
|
|
|
15
21
|
passed: boolean;
|
|
16
22
|
failures: string[];
|
|
17
23
|
};
|
|
24
|
+
ask?: GateBlockAsk;
|
|
18
25
|
}
|
|
19
26
|
/** One logged observed-write tamper (a LOCKED artifact whose hash no longer matches). */
|
|
20
27
|
export interface ObservedTamperRecord {
|