@dzhechkov/harness-core 0.4.5 → 0.5.1
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/.dz-manifest.json +489 -105
- package/README.md +116 -5
- package/dist/agents-policy.d.ts +67 -0
- package/dist/agents-policy.d.ts.map +1 -0
- package/dist/agents-policy.js +258 -0
- package/dist/agents-policy.js.map +1 -0
- package/dist/codex-hooks-assets.d.ts +47 -0
- package/dist/codex-hooks-assets.d.ts.map +1 -0
- package/dist/codex-hooks-assets.js +324 -0
- package/dist/codex-hooks-assets.js.map +1 -0
- package/dist/codex-hooks-verify.d.ts +95 -0
- package/dist/codex-hooks-verify.d.ts.map +1 -0
- package/dist/codex-hooks-verify.js +169 -0
- package/dist/codex-hooks-verify.js.map +1 -0
- package/dist/codex-hooks.d.ts +341 -0
- package/dist/codex-hooks.d.ts.map +1 -0
- package/dist/codex-hooks.js +541 -0
- package/dist/codex-hooks.js.map +1 -0
- package/dist/discrimination-gate.d.ts +88 -15
- package/dist/discrimination-gate.d.ts.map +1 -1
- package/dist/discrimination-gate.js +343 -51
- package/dist/discrimination-gate.js.map +1 -1
- package/dist/feature-adr-checkpoints.d.ts +22 -0
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +42 -0
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +218 -5
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +583 -54
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard.d.ts +13 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +25 -1
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +23 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -4
- package/dist/index.js.map +1 -1
- package/dist/loop-blobs.generated.d.ts +1 -1
- package/dist/loop-blobs.generated.d.ts.map +1 -1
- package/dist/loop-blobs.generated.js +12 -3
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/loop-plan.d.ts +70 -0
- package/dist/loop-plan.d.ts.map +1 -1
- package/dist/loop-plan.js +103 -0
- package/dist/loop-plan.js.map +1 -1
- package/dist/loop-render.d.ts.map +1 -1
- package/dist/loop-render.js +37 -129
- package/dist/loop-render.js.map +1 -1
- package/dist/loop-run-semantics.d.ts +130 -0
- package/dist/loop-run-semantics.d.ts.map +1 -0
- package/dist/loop-run-semantics.js +257 -0
- package/dist/loop-run-semantics.js.map +1 -0
- package/dist/loop-trace.d.ts +31 -2
- package/dist/loop-trace.d.ts.map +1 -1
- package/dist/loop-trace.js +89 -12
- package/dist/loop-trace.js.map +1 -1
- package/dist/managed-hooks.d.ts +86 -0
- package/dist/managed-hooks.d.ts.map +1 -0
- package/dist/managed-hooks.js +101 -0
- package/dist/managed-hooks.js.map +1 -0
- package/dist/mutation-gate.d.ts +14 -0
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +25 -2
- package/dist/mutation-gate.js.map +1 -1
- package/dist/named-lock.d.ts +57 -0
- package/dist/named-lock.d.ts.map +1 -0
- package/dist/named-lock.js +247 -0
- package/dist/named-lock.js.map +1 -0
- package/dist/operations.d.ts +229 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +880 -26
- package/dist/operations.js.map +1 -1
- package/dist/parity.d.ts +106 -1
- package/dist/parity.d.ts.map +1 -1
- package/dist/parity.js +209 -5
- package/dist/parity.js.map +1 -1
- package/dist/qe-bridge.d.ts +291 -0
- package/dist/qe-bridge.d.ts.map +1 -0
- package/dist/qe-bridge.js +538 -0
- package/dist/qe-bridge.js.map +1 -0
- package/dist/recall-usage.d.ts +53 -0
- package/dist/recall-usage.d.ts.map +1 -1
- package/dist/recall-usage.js +125 -2
- package/dist/recall-usage.js.map +1 -1
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +43 -9
- package/dist/score.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +14 -26
- package/dist/setup.js.map +1 -1
- package/dist/shell-veto-policy.d.ts +53 -0
- package/dist/shell-veto-policy.d.ts.map +1 -0
- package/dist/shell-veto-policy.js +103 -0
- package/dist/shell-veto-policy.js.map +1 -0
- package/dist/skills.d.ts +86 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +116 -1
- package/dist/skills.js.map +1 -1
- package/dist/targets.d.ts +75 -0
- package/dist/targets.d.ts.map +1 -1
- package/dist/targets.js +160 -0
- package/dist/targets.js.map +1 -1
- package/dist/workflow-run-dispatch.d.ts +230 -0
- package/dist/workflow-run-dispatch.d.ts.map +1 -0
- package/dist/workflow-run-dispatch.js +363 -0
- package/dist/workflow-run-dispatch.js.map +1 -0
- package/dist/workflow-run.d.ts +498 -0
- package/dist/workflow-run.d.ts.map +1 -0
- package/dist/workflow-run.js +1359 -0
- package/dist/workflow-run.js.map +1 -0
- package/package.json +6 -5
- package/sbom.json +1099 -139
- package/src/agents-policy.ts +338 -0
- package/src/codex-hooks-assets.ts +328 -0
- package/src/codex-hooks-verify.ts +237 -0
- package/src/codex-hooks.ts +723 -0
- package/src/discrimination-gate.ts +456 -58
- package/src/feature-adr-checkpoints.ts +38 -0
- package/src/feature-adr-routing.ts +697 -75
- package/src/guard.ts +36 -1
- package/src/index.ts +162 -2
- package/src/loop-blobs.generated.ts +12 -3
- package/src/loop-plan.ts +185 -0
- package/src/loop-render.ts +37 -127
- package/src/loop-run-semantics.ts +278 -0
- package/src/loop-trace.ts +84 -10
- package/src/managed-hooks.ts +150 -0
- package/src/mutation-gate.ts +24 -2
- package/src/named-lock.ts +277 -0
- package/src/operations.ts +1121 -29
- package/src/parity.ts +295 -6
- package/src/qe-bridge.ts +737 -0
- package/src/recall-usage.ts +184 -1
- package/src/score.ts +50 -9
- package/src/setup.ts +26 -27
- package/src/shell-veto-policy.ts +119 -0
- package/src/skills.ts +174 -1
- package/src/targets.ts +189 -0
- package/src/workflow-run-dispatch.ts +459 -0
- package/src/workflow-run.ts +1743 -0
package/src/parity.ts
CHANGED
|
@@ -23,7 +23,13 @@ import type { TargetName } from './targets.js';
|
|
|
23
23
|
export type RuntimeCapability =
|
|
24
24
|
| 'shell' // can run the dz CLI, i.e. a shell WITH Node.js (what `npm i -g` implies on a dev machine)
|
|
25
25
|
| 'skills' // consumes compiled skills (all adapters emit them; agents-md as one merged file)
|
|
26
|
-
|
|
26
|
+
// The blanket `hooks` capability is deliberately GONE (AM-21). The matrix is COMPUTED, so one
|
|
27
|
+
// word here moves several cells: granting a target `hooks` promoted BOTH `claim-check` (a
|
|
28
|
+
// GATE_FEATURE_IDS member) and `learning-apply` to `full` in one edit, and a caveat comment
|
|
29
|
+
// cannot move a computed cell back.
|
|
30
|
+
| 'hooks-write' // PreToolUse on FILE writes — what claim-check's automatic form needs
|
|
31
|
+
| 'hooks-shell' // PreToolUse on SHELL commands — what the codex veto leg ships
|
|
32
|
+
| 'hooks-prompt' // UserPromptSubmit — what the auto-recall apply leg ships
|
|
27
33
|
| 'mcp' // Model Context Protocol client exists on the platform (servers still need configuring)
|
|
28
34
|
| 'mcp-configured' // our MCP servers (AgentDB / agentic-qe) are wired up out of the box (.mcp.json)
|
|
29
35
|
| 'workflows' // deterministic multi-agent Workflow runtime (ultracode feature-adr, delivery gate)
|
|
@@ -33,7 +39,9 @@ export type RuntimeCapability =
|
|
|
33
39
|
export const RUNTIME_CAPABILITIES: readonly RuntimeCapability[] = [
|
|
34
40
|
'shell',
|
|
35
41
|
'skills',
|
|
36
|
-
'hooks',
|
|
42
|
+
'hooks-write',
|
|
43
|
+
'hooks-shell',
|
|
44
|
+
'hooks-prompt',
|
|
37
45
|
'mcp',
|
|
38
46
|
'mcp-configured',
|
|
39
47
|
'workflows',
|
|
@@ -47,9 +55,18 @@ export const RUNTIME_CAPABILITIES: readonly RuntimeCapability[] = [
|
|
|
47
55
|
export const TARGET_CAPABILITIES: Record<TargetName, readonly RuntimeCapability[]> = {
|
|
48
56
|
// Daily-driven harness: hooks (claim-check PreToolUse, recall UserPromptSubmit), MCP servers,
|
|
49
57
|
// Workflow runtime and the statusline are all exercised in this repo every session.
|
|
50
|
-
'claude-code': ['shell', 'skills', 'hooks', 'mcp', 'mcp-configured', 'workflows', 'statusline'],
|
|
58
|
+
'claude-code': ['shell', 'skills', 'hooks-write', 'hooks-shell', 'hooks-prompt', 'mcp', 'mcp-configured', 'workflows', 'statusline'],
|
|
51
59
|
// MCP subsystem probed live: `codex mcp --help` + `codex mcp list` answer (2026-07-19).
|
|
52
|
-
|
|
60
|
+
// hooks-shell + hooks-prompt: user-global `$CODEX_HOME/hooks.json`, installed and ARMED
|
|
61
|
+
// unattended, proved by a live two-sided block probe (2026-08-19, codex-cli 0.147.0 — the
|
|
62
|
+
// transcripts named in CAPABILITY_EVIDENCE). `hooks-write` is NOT granted: this leg ships no
|
|
63
|
+
// PreToolUse guard on Write/Edit, so `claim-check` stays `manual` on codex.
|
|
64
|
+
//
|
|
65
|
+
// Fact CORRECTED 2026-08-19: a project-level `<repo>/.codex/hooks.json` DOES load on 0.147.0
|
|
66
|
+
// (`source: "project"`, MEASURED). The earlier note that project-level files are ignored was
|
|
67
|
+
// measured on 0.144.6 and is stale. dz still writes only the user-global registry, but that is
|
|
68
|
+
// now a DECISION (one carrier, one removable unit) rather than a description of the runtime.
|
|
69
|
+
codex: ['shell', 'skills', 'mcp', 'hooks-shell', 'hooks-prompt'],
|
|
53
70
|
// Conservative v1: skills emission verified by the adapters; richer runtimes unproven.
|
|
54
71
|
opencode: ['shell', 'skills'],
|
|
55
72
|
hermes: ['shell', 'skills'],
|
|
@@ -62,6 +79,242 @@ export const TARGET_CAPABILITIES: Record<TargetName, readonly RuntimeCapability[
|
|
|
62
79
|
windsurf: ['shell', 'skills'],
|
|
63
80
|
};
|
|
64
81
|
|
|
82
|
+
/**
|
|
83
|
+
* WHY each capability grant is believed, as machine-readable DATA (AM-22).
|
|
84
|
+
*
|
|
85
|
+
* `parity.ts` has declared an honesty contract in prose since day one — *"a capability flag states
|
|
86
|
+
* only what has been VERIFIED, each with its source in a comment"*. A comment is documentation, not
|
|
87
|
+
* a gate. The pinned map in `parity.test.ts` catches an ACCIDENTAL capability, but updating a pin is
|
|
88
|
+
* a mechanical edit that demands no evidence, so the contract had no layer-1 half.
|
|
89
|
+
*
|
|
90
|
+
* This is that half. `parity_no_capability_grant_without_evidence` fails when a target declares a
|
|
91
|
+
* capability with no record here, when a `transcript` record points at a file that does not exist,
|
|
92
|
+
* or when **any cell computes `full` on a target whose deciding capability has no transcript**.
|
|
93
|
+
*
|
|
94
|
+
* `kind: 'transcript'` means a recorded live run is on disk at `evidence`; `kind: 'reproducer'`
|
|
95
|
+
* means `evidence` is a command anyone can re-run.
|
|
96
|
+
*/
|
|
97
|
+
export interface CapabilityEvidence {
|
|
98
|
+
readonly evidence: string;
|
|
99
|
+
readonly kind: 'transcript' | 'reproducer';
|
|
100
|
+
/** ISO date the evidence was produced. Stale evidence is still evidence — silence is not. */
|
|
101
|
+
readonly at: string;
|
|
102
|
+
/**
|
|
103
|
+
* The RUNTIME VERSION the evidence was measured on, verbatim from the runtime's own `--version`
|
|
104
|
+
* (e.g. `codex-cli 0.147.0`).
|
|
105
|
+
*
|
|
106
|
+
* ADR-006 pins capability grants to a runtime version, and the gate enforced only that a record
|
|
107
|
+
* existed and its file was on disk — so evidence recorded on codex 0.147 kept a grant alive on
|
|
108
|
+
* the installed 0.148 (independent review, finding 3). A hook registry is exactly the kind of
|
|
109
|
+
* surface a minor version moves. When this is set and the caller supplies a PROBED version that
|
|
110
|
+
* differs, the grant is reported `stale-runtime-version` — INCONCLUSIVE, not revoked: the fix is
|
|
111
|
+
* a fresh probe, and an absent probe flips nothing.
|
|
112
|
+
*/
|
|
113
|
+
readonly runtimeVersion?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const CAPABILITY_EVIDENCE: Record<TargetName, Partial<Record<RuntimeCapability, CapabilityEvidence>>> = {
|
|
117
|
+
'claude-code': {
|
|
118
|
+
shell: { evidence: 'dz --version', kind: 'reproducer', at: '2026-07-19' },
|
|
119
|
+
skills: { evidence: 'dz compose --target claude-code', kind: 'reproducer', at: '2026-07-19' },
|
|
120
|
+
'hooks-write': { evidence: 'grep -n claim-check-hook .claude/settings.json', kind: 'reproducer', at: '2026-07-19' },
|
|
121
|
+
'hooks-shell': { evidence: 'grep -n PreToolUse .claude/settings.json', kind: 'reproducer', at: '2026-07-19' },
|
|
122
|
+
'hooks-prompt': { evidence: 'grep -n recall-hook.cjs .claude/settings.json', kind: 'reproducer', at: '2026-07-19' },
|
|
123
|
+
mcp: { evidence: 'cat .mcp.json', kind: 'reproducer', at: '2026-07-19' },
|
|
124
|
+
'mcp-configured': { evidence: 'cat .mcp.json', kind: 'reproducer', at: '2026-07-19' },
|
|
125
|
+
workflows: { evidence: 'ls .claude/workflows/feature-adr.js', kind: 'reproducer', at: '2026-07-19' },
|
|
126
|
+
statusline: { evidence: 'dz statusline', kind: 'reproducer', at: '2026-07-19' },
|
|
127
|
+
},
|
|
128
|
+
codex: {
|
|
129
|
+
shell: { evidence: 'codex exec -m <id> "Reply with exactly: OK"', kind: 'reproducer', at: '2026-07-19' },
|
|
130
|
+
skills: { evidence: 'dz compose --target codex', kind: 'reproducer', at: '2026-07-19' },
|
|
131
|
+
mcp: { evidence: 'codex mcp list', kind: 'reproducer', at: '2026-07-19' },
|
|
132
|
+
// The two-sided live block: our marker in the transcript AND the sentinel side effect absent,
|
|
133
|
+
// in a NON-bypassed run, with the entry reported `trusted` by codex's own `hooks/list`.
|
|
134
|
+
// RE-PROBED on the installed runtime at the independent-QE fix round (review finding 3): the
|
|
135
|
+
// 0.147.0 evidence was stale on a machine running 0.148.0, and re-running it was NOT a
|
|
136
|
+
// formality — the arming path had silently stopped working, because `hooks/list` spells its
|
|
137
|
+
// `eventName` field `preToolUse` on 0.148 while the trust KEY still says `pre_tool_use`.
|
|
138
|
+
// All four legs pass on 0.148.0 with the trust axis PARSED from `hooks/list` rather than
|
|
139
|
+
// hard-coded (the pre-fix grader asserted `trustStatus: 'trusted'` as a constant).
|
|
140
|
+
'hooks-shell': {
|
|
141
|
+
evidence: 'features/crossrt-2-codex-hooks/07_code_changes/probe-results/fixround/veto-armed.txt',
|
|
142
|
+
kind: 'transcript',
|
|
143
|
+
at: '2026-08-19',
|
|
144
|
+
runtimeVersion: 'codex-cli 0.148.0',
|
|
145
|
+
},
|
|
146
|
+
// NOT re-probed on 0.148: the recall canary is a separate live run (forced hit + removed twin)
|
|
147
|
+
// and this round did not execute it. The version it was measured on is recorded HONESTLY, so
|
|
148
|
+
// `findUnbackedCapabilities` reports it `stale-runtime-version` — INCONCLUSIVE — the moment a
|
|
149
|
+
// caller supplies the probed 0.148.0. That is the correct state: the grant is not withdrawn,
|
|
150
|
+
// it is awaiting its re-probe.
|
|
151
|
+
'hooks-prompt': {
|
|
152
|
+
evidence: 'features/crossrt-2-codex-hooks/07_code_changes/probe-results/recall-canary.md',
|
|
153
|
+
kind: 'transcript',
|
|
154
|
+
at: '2026-08-19',
|
|
155
|
+
runtimeVersion: 'codex-cli 0.147.0',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
opencode: { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target opencode', kind: 'reproducer', at: '2026-07-19' } },
|
|
159
|
+
hermes: { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target hermes', kind: 'reproducer', at: '2026-07-19' } },
|
|
160
|
+
openclaude: { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target openclaude', kind: 'reproducer', at: '2026-07-19' } },
|
|
161
|
+
copilot: { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target copilot', kind: 'reproducer', at: '2026-07-19' } },
|
|
162
|
+
'agents-md': { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target agents-md', kind: 'reproducer', at: '2026-07-19' } },
|
|
163
|
+
cursor: { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target cursor', kind: 'reproducer', at: '2026-07-19' } },
|
|
164
|
+
gemini: { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target gemini', kind: 'reproducer', at: '2026-07-19' } },
|
|
165
|
+
windsurf: { shell: { evidence: 'adapter emit', kind: 'reproducer', at: '2026-07-19' }, skills: { evidence: 'dz compose --target windsurf', kind: 'reproducer', at: '2026-07-19' } },
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export interface UnbackedCapability {
|
|
169
|
+
readonly target: TargetName;
|
|
170
|
+
readonly capability: RuntimeCapability;
|
|
171
|
+
/**
|
|
172
|
+
* `stale-runtime-version` is INCONCLUSIVE, not a refutation: the grant was proven, on a runtime
|
|
173
|
+
* that is no longer the installed one. It clears when a current-version probe is recorded.
|
|
174
|
+
*/
|
|
175
|
+
readonly reason: 'no-evidence-record' | 'dangling-transcript' | 'stale-runtime-version';
|
|
176
|
+
readonly evidence?: string;
|
|
177
|
+
/** `recorded → probed`, for the stale case. */
|
|
178
|
+
readonly recordedVersion?: string;
|
|
179
|
+
readonly probedVersion?: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Runtime versions observed NOW, keyed by target — injected, never probed here (this module is
|
|
184
|
+
* pure). An absent entry means "not probed", which flips nothing.
|
|
185
|
+
*/
|
|
186
|
+
export type ProbedRuntimeVersions = Partial<Record<TargetName, string>>;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Every capability grant that is NOT backed by usable evidence.
|
|
190
|
+
*
|
|
191
|
+
* PURE, with the filesystem injected as `transcriptExists`. That is deliberate: the property this
|
|
192
|
+
* enforces — *a grant with a dangling transcript is not a grant* — has to be provable without a
|
|
193
|
+
* repository on disk, or the mutation gate (which copies the PACKAGE, not the repo) could never
|
|
194
|
+
* turn its mutant red, and an unkillable mutant is a false green wearing a gate's clothes.
|
|
195
|
+
*/
|
|
196
|
+
export function findUnbackedCapabilities(
|
|
197
|
+
transcriptExists: (path: string) => boolean,
|
|
198
|
+
capabilities: Record<TargetName, readonly RuntimeCapability[]> = TARGET_CAPABILITIES,
|
|
199
|
+
evidence: Record<TargetName, Partial<Record<RuntimeCapability, CapabilityEvidence>>> = CAPABILITY_EVIDENCE,
|
|
200
|
+
probedVersions: ProbedRuntimeVersions = {},
|
|
201
|
+
): UnbackedCapability[] {
|
|
202
|
+
const out: UnbackedCapability[] = [];
|
|
203
|
+
for (const target of Object.keys(capabilities) as TargetName[]) {
|
|
204
|
+
for (const capability of capabilities[target]) {
|
|
205
|
+
const record = evidence[target]?.[capability];
|
|
206
|
+
if (record === undefined) {
|
|
207
|
+
out.push({ target, capability, reason: 'no-evidence-record' });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (record.kind === 'transcript' && !transcriptExists(record.evidence)) {
|
|
211
|
+
out.push({ target, capability, reason: 'dangling-transcript', evidence: record.evidence });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
// Version pin (ADR-006, finding 3). Only fires when BOTH sides are known: an unprobed target
|
|
215
|
+
// stays silent rather than reporting every grant stale on a machine without the runtime.
|
|
216
|
+
const probed = probedVersions[target];
|
|
217
|
+
if (record.runtimeVersion !== undefined && probed !== undefined && probed !== record.runtimeVersion) {
|
|
218
|
+
out.push({
|
|
219
|
+
target,
|
|
220
|
+
capability,
|
|
221
|
+
reason: 'stale-runtime-version',
|
|
222
|
+
evidence: record.evidence,
|
|
223
|
+
recordedVersion: record.runtimeVersion,
|
|
224
|
+
probedVersion: probed,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Compare two recorded runtime versions the way a version string means it.
|
|
234
|
+
*
|
|
235
|
+
* `null` when the two are not comparable (different product, unparseable) — which the caller reads
|
|
236
|
+
* as "not the newest", the conservative direction.
|
|
237
|
+
*/
|
|
238
|
+
function compareRuntimeVersions(a: string, b: string): number | null {
|
|
239
|
+
const parse = (v: string): { product: string; parts: number[] } | null => {
|
|
240
|
+
const m = /^(.*?)\s*(\d+(?:\.\d+)*)\s*$/.exec(v.trim());
|
|
241
|
+
if (m === null) return null;
|
|
242
|
+
return { product: (m[1] ?? '').trim(), parts: (m[2] ?? '').split('.').map((n) => Number(n)) };
|
|
243
|
+
};
|
|
244
|
+
const pa = parse(a);
|
|
245
|
+
const pb = parse(b);
|
|
246
|
+
if (pa === null || pb === null || pa.product !== pb.product) return null;
|
|
247
|
+
const len = Math.max(pa.parts.length, pb.parts.length);
|
|
248
|
+
for (let i = 0; i < len; i += 1) {
|
|
249
|
+
const x = pa.parts[i] ?? 0;
|
|
250
|
+
const y = pb.parts[i] ?? 0;
|
|
251
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
252
|
+
}
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The newest runtime version recorded among a target's TRANSCRIPT evidence, or `null`.
|
|
258
|
+
*
|
|
259
|
+
* This is what makes staleness detectable WITHOUT running anything: the records date themselves
|
|
260
|
+
* against each other. `dz parity` computes a matrix and must keep doing so deterministically — a
|
|
261
|
+
* `codex --version` subprocess inside it would make a pure report depend on the machine it prints
|
|
262
|
+
* on. Re-probing one capability is what dates the others (fix round 2, R2-3).
|
|
263
|
+
*/
|
|
264
|
+
export function newestRecordedRuntimeVersion(
|
|
265
|
+
target: TargetName,
|
|
266
|
+
evidence: Record<TargetName, Partial<Record<RuntimeCapability, CapabilityEvidence>>> = CAPABILITY_EVIDENCE,
|
|
267
|
+
): string | null {
|
|
268
|
+
let newest: string | null = null;
|
|
269
|
+
for (const record of Object.values(evidence[target] ?? {})) {
|
|
270
|
+
if (record === undefined || record.kind !== 'transcript' || record.runtimeVersion === undefined) continue;
|
|
271
|
+
if (newest === null) {
|
|
272
|
+
newest = record.runtimeVersion;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const cmp = compareRuntimeVersions(newest, record.runtimeVersion);
|
|
276
|
+
if (cmp !== null && cmp < 0) newest = record.runtimeVersion;
|
|
277
|
+
}
|
|
278
|
+
return newest;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Transcript evidence that is out of date with the newest record for its own target.
|
|
283
|
+
*
|
|
284
|
+
* SCOPE, stated because it is a judgement and not a derivation: the version rule applies to
|
|
285
|
+
* `kind: 'transcript'` records only. A transcript FREEZES one runtime moment, so it can go stale; a
|
|
286
|
+
* `reproducer` is a command anyone can re-run, and calling it stale would report `dz --version` as
|
|
287
|
+
* expired. A transcript with NO `runtimeVersion` is stale too — an undated observation cannot be
|
|
288
|
+
* shown to be current.
|
|
289
|
+
*
|
|
290
|
+
* INCONCLUSIVE, never a refutation: the grant is not withdrawn, it is awaiting its re-probe.
|
|
291
|
+
*/
|
|
292
|
+
export function findStaleTranscriptEvidence(
|
|
293
|
+
capabilities: Record<TargetName, readonly RuntimeCapability[]> = TARGET_CAPABILITIES,
|
|
294
|
+
evidence: Record<TargetName, Partial<Record<RuntimeCapability, CapabilityEvidence>>> = CAPABILITY_EVIDENCE,
|
|
295
|
+
): UnbackedCapability[] {
|
|
296
|
+
const out: UnbackedCapability[] = [];
|
|
297
|
+
for (const target of Object.keys(capabilities) as TargetName[]) {
|
|
298
|
+
const newest = newestRecordedRuntimeVersion(target, evidence);
|
|
299
|
+
if (newest === null) continue; // nothing version-pinned on this target ⇒ nothing to be stale against
|
|
300
|
+
for (const capability of capabilities[target]) {
|
|
301
|
+
const record = evidence[target]?.[capability];
|
|
302
|
+
if (record === undefined || record.kind !== 'transcript') continue;
|
|
303
|
+
const recorded = record.runtimeVersion;
|
|
304
|
+
if (recorded !== undefined && compareRuntimeVersions(recorded, newest) === 0) continue;
|
|
305
|
+
out.push({
|
|
306
|
+
target,
|
|
307
|
+
capability,
|
|
308
|
+
reason: 'stale-runtime-version',
|
|
309
|
+
evidence: record.evidence,
|
|
310
|
+
...(recorded !== undefined ? { recordedVersion: recorded } : {}),
|
|
311
|
+
probedVersion: newest,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
|
|
65
318
|
/** How a feature manifests on a platform: a concrete FORM with its runtime requirements. */
|
|
66
319
|
export interface FeatureForm {
|
|
67
320
|
/** Human-readable name of the form, shown as the `via` of a parity cell (AM-2). */
|
|
@@ -124,7 +377,7 @@ export const PARITY_FEATURES: readonly ParityFeature[] = [
|
|
|
124
377
|
id: 'claim-check',
|
|
125
378
|
title: 'Integrity claim-check',
|
|
126
379
|
forms: [
|
|
127
|
-
{ form: 'PreToolUse hook (automatic on Write/Edit)', requires: ['hooks'], level: 'full' },
|
|
380
|
+
{ form: 'PreToolUse hook (automatic on Write/Edit)', requires: ['hooks-write'], level: 'full' },
|
|
128
381
|
{ form: 'dz claim-check (CLI) + publish gate', requires: ['shell'], level: 'manual' },
|
|
129
382
|
],
|
|
130
383
|
},
|
|
@@ -137,7 +390,7 @@ export const PARITY_FEATURES: readonly ParityFeature[] = [
|
|
|
137
390
|
id: 'learning-apply',
|
|
138
391
|
title: 'Self-learning: automatic apply-leg',
|
|
139
392
|
forms: [
|
|
140
|
-
{ form: 'UserPromptSubmit hook (auto recall)', requires: ['hooks'], level: 'full' },
|
|
393
|
+
{ form: 'UserPromptSubmit hook (auto recall)', requires: ['hooks-prompt'], level: 'full' },
|
|
141
394
|
{ form: 'manual dz recall before a task', requires: ['shell'], level: 'manual' },
|
|
142
395
|
],
|
|
143
396
|
},
|
|
@@ -215,6 +468,42 @@ export function computeParity(feature: ParityFeature, capabilities: readonly Run
|
|
|
215
468
|
return { level: 'none' };
|
|
216
469
|
}
|
|
217
470
|
|
|
471
|
+
/**
|
|
472
|
+
* A parity cell AS REPORTED — the computed level, plus the one thing the computation cannot know:
|
|
473
|
+
* whether the evidence behind the deciding capability is still current.
|
|
474
|
+
*
|
|
475
|
+
* `inconclusive` is a REPORTING level, not a model level: `computeParity` keeps answering the
|
|
476
|
+
* capability question, and this layer answers the evidence question. Keeping them apart is what
|
|
477
|
+
* lets the matrix stay a pure function of the capability model.
|
|
478
|
+
*/
|
|
479
|
+
export interface ParityReportCell {
|
|
480
|
+
readonly level: 'full' | 'manual' | 'none' | 'inconclusive';
|
|
481
|
+
readonly via?: string | undefined;
|
|
482
|
+
/** The stale capabilities the deciding form depends on (present only when `inconclusive`). */
|
|
483
|
+
readonly staleEvidence?: readonly RuntimeCapability[];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Downgrade a cell whose DECIDING form rests on stale evidence to `inconclusive`.
|
|
488
|
+
*
|
|
489
|
+
* The round-1 gate could tell that `hooks-prompt` evidence was stale and nothing a user runs ever
|
|
490
|
+
* asked it (fix round 2, R2-3): `dz parity` printed `full` for the auto-recall leg on codex off a
|
|
491
|
+
* transcript recorded on a runtime that is no longer installed. A cell that says `full` on evidence
|
|
492
|
+
* nobody has re-confirmed is the same class of claim this whole feature exists to refuse.
|
|
493
|
+
*/
|
|
494
|
+
export function downgradeForStaleEvidence(
|
|
495
|
+
feature: ParityFeature,
|
|
496
|
+
cell: ParityCell,
|
|
497
|
+
staleCapabilities: readonly RuntimeCapability[],
|
|
498
|
+
): ParityReportCell {
|
|
499
|
+
if (cell.level === 'none' || cell.via === undefined || staleCapabilities.length === 0) return cell;
|
|
500
|
+
const form = feature.forms.find((f) => f.form === cell.via);
|
|
501
|
+
if (form === undefined) return cell;
|
|
502
|
+
const hit = form.requires.filter((r) => staleCapabilities.includes(r));
|
|
503
|
+
if (hit.length === 0) return cell;
|
|
504
|
+
return { level: 'inconclusive', via: cell.via, staleEvidence: hit };
|
|
505
|
+
}
|
|
506
|
+
|
|
218
507
|
export interface ParityMatrixRow {
|
|
219
508
|
readonly feature: ParityFeature;
|
|
220
509
|
readonly cells: Readonly<Record<TargetName, ParityCell>>;
|