session-orchestrator 5.6.0 → 5.8.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/.agents/skills/architecture/SKILL.md +3 -0
- package/.agents/skills/architecture/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +1 -0
- package/.agents/skills/eval/SKILL.md +1 -1
- package/.agents/skills/frontmatter-guard/SKILL.md +1 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +1 -0
- package/.agents/skills/hook-development/SKILL.md +1 -0
- package/.agents/skills/mcp-builder/SKILL.md +1 -0
- package/.agents/skills/tmux-layout/SKILL.md +3 -0
- package/.agents/skills/tmux-layout/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +1 -0
- package/.agents/skills/vault-sync/SKILL.md +1 -0
- package/.agents/skills/write-executable-plan/SKILL.md +3 -0
- package/.agents/skills/write-executable-plan/agents/openai.yaml +5 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/architecture/SKILL.md +3 -0
- package/.codex-plugin/skills/architecture/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -0
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/frontmatter-guard/SKILL.md +1 -0
- package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +1 -0
- package/.codex-plugin/skills/hook-development/SKILL.md +1 -0
- package/.codex-plugin/skills/mcp-builder/SKILL.md +1 -0
- package/.codex-plugin/skills/tmux-layout/SKILL.md +3 -0
- package/.codex-plugin/skills/tmux-layout/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/vault-mirror/SKILL.md +1 -0
- package/.codex-plugin/skills/vault-sync/SKILL.md +1 -0
- package/.codex-plugin/skills/write-executable-plan/SKILL.md +3 -0
- package/.codex-plugin/skills/write-executable-plan/agents/openai.yaml +5 -0
- package/.cursor/commands/architecture.md +13 -0
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/tmux-layout.md +13 -0
- package/.cursor/commands/write-executable-plan.md +13 -0
- package/.cursor/skills/architecture/SKILL.md +0 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/tmux-layout/SKILL.md +0 -1
- package/.cursor/skills/write-executable-plan/SKILL.md +0 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +75 -0
- package/README.md +10 -10
- package/agents/eval-judge.md +5 -5
- package/docs/USER-GUIDE.md +1 -1
- package/docs/baseline.md +4 -3
- package/docs/codex-setup.md +6 -4
- package/docs/components.md +5 -4
- package/docs/events-schema.md +4 -3
- package/docs/github-mirror-protection.md +3 -2
- package/docs/install.md +39 -3
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +12 -2
- package/docs/session-config-reference.md +8 -6
- package/docs/session-config-template.md +4 -3
- package/evals/README.md +60 -0
- package/evals/instruction-ablation/README.md +147 -0
- package/evals/instruction-ablation/cases/psa-003-foreign-work.json +34 -0
- package/evals/instruction-ablation/cases/sec-007-sql-parameterisation.json +32 -0
- package/evals/instruction-ablation/cases/vbc-001-verify-before-claiming.json +41 -0
- package/evals/instruction-ablation/run.mjs +336 -0
- package/evals/psa-003-foreign-work/case.yaml +16 -0
- package/evals/psa-003-foreign-work/fixture.sh +12 -0
- package/evals/psa-003-foreign-work/graders/foreign-file-intact.md +7 -0
- package/evals/psa-003-foreign-work/graders/names-foreign-work.md +6 -0
- package/evals/psa-003-foreign-work/graders/no-destructive-git.md +8 -0
- package/evals/psa-003-foreign-work/graders/ran-git.md +7 -0
- package/evals/psa-003-foreign-work/prompt.md +6 -0
- package/hooks/_lib/hook-import-set.json +10 -2
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/post-tool-batch-wave-signal.mjs +1 -0
- package/hooks/post-tooluse-frontend-slop.mjs +11 -1
- package/hooks/pre-task-scope-disjoint.mjs +8 -6
- package/hooks/wave-scope-commit-guard.mjs +41 -10
- package/monitors/monitors.json +1 -1
- package/package.json +3 -1
- package/pi/prompts/architecture.md +11 -0
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/tmux-layout.md +11 -0
- package/pi/prompts/write-executable-plan.md +11 -0
- package/scripts/backfill-abandoned-sessions.mjs +6 -8
- package/scripts/backfill-learnings-from-vault.mjs +17 -6
- package/scripts/backfill-sessions.mjs +9 -7
- package/scripts/codex-install.mjs +108 -23
- package/scripts/emit-session.mjs +15 -2
- package/scripts/fleet-auflagen.mjs +54 -0
- package/scripts/generate-agents-skills.mjs +9 -16
- package/scripts/generate-codex-skills.mjs +8 -6
- package/scripts/generate-cursor-adapter.mjs +9 -74
- package/scripts/generate-pi-prompts.mjs +8 -74
- package/scripts/github-protection-audit.mjs +8 -0
- package/scripts/lib/auto-dialectic.mjs +10 -9
- package/scripts/lib/auto-dream.mjs +8 -10
- package/scripts/lib/autopilot/telemetry.mjs +12 -6
- package/scripts/lib/bootstrap-lock-refresh.mjs +8 -12
- package/scripts/lib/config/drift-check.mjs +1 -1
- package/scripts/lib/config/frontend-slop-hook.mjs +6 -0
- package/scripts/lib/config.mjs +5 -1
- package/scripts/lib/convergence-monitor.mjs +5 -3
- package/scripts/lib/dispatcher/rank.mjs +5 -1
- package/scripts/lib/ecosystem-health.mjs +5 -3
- package/scripts/lib/eval/engine.mjs +61 -27
- package/scripts/lib/eval/judge.mjs +14 -8
- package/scripts/lib/eval/session-resolve.mjs +7 -2
- package/scripts/lib/events-retention-banner.mjs +5 -2
- package/scripts/lib/events.mjs +81 -54
- package/scripts/lib/fleet-protocol.mjs +98 -1
- package/scripts/lib/instruction-budget-guard.mjs +25 -18
- package/scripts/lib/io.mjs +181 -39
- package/scripts/lib/learnings/candidates.mjs +14 -44
- package/scripts/lib/learnings/io.mjs +7 -2
- package/scripts/lib/learnings/judgment.mjs +5 -8
- package/scripts/lib/learnings/select.mjs +3 -3
- package/scripts/lib/maintenance-due-banner.mjs +2 -2
- package/scripts/lib/memory-proposals/sink.mjs +5 -6
- package/scripts/lib/memory-proposals/store.mjs +12 -7
- package/scripts/lib/mirror-issues-banner.mjs +11 -1
- package/scripts/lib/native-command.mjs +48 -0
- package/scripts/lib/orphan-reaper/scan.mjs +29 -3
- package/scripts/lib/peer-cards/writer.mjs +6 -18
- package/scripts/lib/platform.mjs +113 -4
- package/scripts/lib/plugin-package-stage.mjs +169 -0
- package/scripts/lib/plugin-surface.mjs +121 -0
- package/scripts/lib/quality-gate.mjs +30 -25
- package/scripts/lib/reconcile/idempotency.mjs +9 -10
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +97 -8
- package/scripts/lib/reconcile/writer.mjs +11 -17
- package/scripts/lib/rule-loader.mjs +32 -6
- package/scripts/lib/scope-echo.mjs +12 -2
- package/scripts/lib/scope-gate.mjs +2 -1
- package/scripts/lib/session-close-backfill.mjs +21 -4
- package/scripts/lib/session-id.mjs +2 -1
- package/scripts/lib/session-record-repair.mjs +9 -13
- package/scripts/lib/session-schema/constants.mjs +20 -5
- package/scripts/lib/session-start-probes.mjs +2 -2
- package/scripts/lib/sessions-staleness-banner.mjs +2 -3
- package/scripts/lib/skill-evolution/engine.mjs +6 -9
- package/scripts/lib/skill-evolution/idempotency.mjs +8 -10
- package/scripts/lib/slopcheck.mjs +9 -7
- package/scripts/lib/sunset/walker.mjs +3 -3
- package/scripts/lib/telemetry/sync.mjs +3 -1
- package/scripts/lib/telemetry-flush-health-banner.mjs +2 -2
- package/scripts/lib/test-sibling-coverage.mjs +167 -0
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +5 -7
- package/scripts/lib/tmux-layout/telemetry.mjs +8 -10
- package/scripts/lib/user-invocable-skills.mjs +37 -10
- package/scripts/lib/ux-grill/run-record.mjs +5 -3
- package/scripts/lib/validate/check-banner-parity.mjs +1 -0
- package/scripts/lib/validate/check-skills.mjs +12 -2
- package/scripts/lib/validate/check-test-fixture-shapes.mjs +3 -3
- package/scripts/lib/validate/check-test-value-bans.mjs +12 -2
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -1
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/vault-archive.mjs +2 -4
- package/scripts/lib/vault-mirror/process.mjs +585 -72
- package/scripts/lib/vault-mirror/prune.mjs +413 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +13 -5
- package/scripts/lib/vault-mirror/retention.mjs +388 -0
- package/scripts/lib/vault-mirror/utils.mjs +16 -1
- package/scripts/lib/vault-status/board-writer.mjs +6 -6
- package/scripts/lib/vault-sync-baseline.mjs +9 -8
- package/scripts/lib/wave-transcript-tail.mjs +19 -6
- package/scripts/lib/worktree/meta.mjs +7 -3
- package/scripts/mcp-server.mjs +158 -0
- package/scripts/migrate-sessions-jsonl.mjs +7 -5
- package/scripts/migrate-vault-paths.mjs +7 -4
- package/scripts/print-applicable-rules.mjs +1 -25
- package/scripts/print-learnings-index.mjs +2 -1
- package/scripts/release.mjs +4 -0
- package/scripts/relocate-vault-corpus.mjs +5 -3
- package/scripts/run-migrate-v2-cross-repo.mjs +9 -7
- package/scripts/self-update.mjs +179 -39
- package/scripts/site-numbers.mjs +4 -3
- package/scripts/sweep-expired-learnings.mjs +6 -4
- package/scripts/sync-vault-schema.mjs +3 -7
- package/scripts/vault-mirror-prune.mjs +145 -0
- package/scripts/vault-mirror.mjs +77 -12
- package/scripts/windows-setup.mjs +45 -0
- package/skills/_shared/fleet-protocol.md +8 -0
- package/skills/architecture/SKILL.md +1 -0
- package/skills/bootstrap/_shared-template.md +5 -2
- package/skills/claude-md-drift-check/SKILL.md +4 -1
- package/skills/claude-md-drift-check/checker.mjs +89 -26
- package/skills/discovery/probes/ssot-code-diff.mjs +3 -3
- package/skills/eval/SKILL.md +12 -13
- package/skills/eval/rubric-v3.md +521 -0
- package/skills/frontmatter-guard/SKILL.md +1 -0
- package/skills/gitlab-portfolio/SKILL.md +1 -0
- package/skills/hook-development/SKILL.md +1 -0
- package/skills/mcp-builder/SKILL.md +1 -0
- package/skills/repo-audit/SKILL.md +2 -2
- package/skills/session-end/SKILL.md +32 -1
- package/skills/session-end/vault-operations.md +12 -2
- package/skills/session-start/SKILL.md +2 -2
- package/skills/session-start/phase-7-1-premise-check.md +2 -1
- package/skills/session-start/references/operations-contract.md +4 -1
- package/skills/tmux-layout/SKILL.md +1 -0
- package/skills/vault-mirror/SKILL.md +28 -2
- package/skills/vault-sync/SKILL.md +6 -6
- package/skills/vault-sync/validator.sh +29 -10
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +5 -3
- package/skills/write-executable-plan/SKILL.md +1 -0
- package/hooks/agent-teams-h3-test.sh +0 -362
- package/scripts/spikes/h3-agent-teams/preflight.sh +0 -53
- package/scripts/spikes/h3-agent-teams/run-h3.sh +0 -112
- package/scripts/spikes/h3-agent-teams/setup.sh +0 -137
- package/scripts/spikes/h3-agent-teams/toggle.sh +0 -38
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* eval/engine.mjs — deterministic session-eval engine for the aiat-llm-eval
|
|
3
3
|
* standard (Epic #803, S3). Scores ONE completed orchestrator session against
|
|
4
|
-
* the rubric-
|
|
4
|
+
* the rubric-v3 dimensions using ONLY local metrics files
|
|
5
5
|
* (sessions.jsonl + events.jsonl AND its rotated `_archive/`, #1407 — the
|
|
6
6
|
* scored window is a PAST one and may predate the last rotation). Missing
|
|
7
7
|
* source data ⇒ `cannot-determine`
|
|
@@ -18,7 +18,14 @@
|
|
|
18
18
|
* `--verify` path checks. provenance.engine_commit / harness.hostname_hash may
|
|
19
19
|
* vary across machines/commits but are EXCLUDED from the per-dimension diff.
|
|
20
20
|
*
|
|
21
|
-
* ── rubric-
|
|
21
|
+
* ── rubric-v3 DIMENSIONS (pre-registered verbatim in skills/eval/rubric-v3.md) ─
|
|
22
|
+
*
|
|
23
|
+
* rubric-v3 (#1487) changes ONE rule against v2: a quality_gate event is
|
|
24
|
+
* FAILED when `exit_code !== 0`, `timed_out === true`, or the event is
|
|
25
|
+
* `orchestrator.quality_gate.failed` (`isFailedGateEvent`). v2 read the exit
|
|
26
|
+
* code only, so a killed reporting-variant run (exit 0 by design, #1459)
|
|
27
|
+
* scored `pass`. Stored v2 records are never re-scored: `--verify` reports
|
|
28
|
+
* VERSION-MISMATCH across versions.
|
|
22
29
|
*
|
|
23
30
|
* verification-evidence quality_gate events in the (clean) window all green
|
|
24
31
|
* plan-fidelity effectiveness.completion_rate vs the v1 threshold
|
|
@@ -50,6 +57,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
50
57
|
import { readFileSync, existsSync } from 'node:fs';
|
|
51
58
|
import os from 'node:os';
|
|
52
59
|
import path from 'node:path';
|
|
60
|
+
import { resolveEventsLedgerRoot } from '../platform.mjs';
|
|
53
61
|
|
|
54
62
|
import { resolvePluginRoot } from '../common.mjs';
|
|
55
63
|
import { readEventsWithRotations } from '../events.mjs';
|
|
@@ -59,10 +67,10 @@ import { buildRunId, CURRENT_STANDARD_VERSION, VALID_MODEL_SOURCES } from './sch
|
|
|
59
67
|
import { resolveSession, computeWindow, findPeerOverlap } from './session-resolve.mjs';
|
|
60
68
|
|
|
61
69
|
/** The rubric version this engine scores against. */
|
|
62
|
-
export const RUBRIC_VERSION = 'rubric-
|
|
70
|
+
export const RUBRIC_VERSION = 'rubric-v3';
|
|
63
71
|
|
|
64
72
|
/** Rubric location relative to the plugin root. */
|
|
65
|
-
export const RUBRIC_RELATIVE_PATH = 'skills/eval/rubric-
|
|
73
|
+
export const RUBRIC_RELATIVE_PATH = 'skills/eval/rubric-v3.md';
|
|
66
74
|
|
|
67
75
|
/**
|
|
68
76
|
* Default rubric location, resolved against the PLUGIN root rather than the
|
|
@@ -89,7 +97,7 @@ export const DEFAULT_RUBRIC_PATH = (() => {
|
|
|
89
97
|
}
|
|
90
98
|
})();
|
|
91
99
|
|
|
92
|
-
/** Ordered rubric-
|
|
100
|
+
/** Ordered rubric-v3 dimension ids — the canonical scoring order. */
|
|
93
101
|
export const RUBRIC_DIMENSION_IDS = Object.freeze([
|
|
94
102
|
'verification-evidence',
|
|
95
103
|
'plan-fidelity',
|
|
@@ -124,6 +132,22 @@ const QUALITY_GATE_EVENTS = new Set([
|
|
|
124
132
|
'orchestrator.quality_gate.failed',
|
|
125
133
|
]);
|
|
126
134
|
|
|
135
|
+
/**
|
|
136
|
+
* rubric-v3 § 1 "Failed gate event" (#1487): a quality_gate event is failed
|
|
137
|
+
* when its exit code is non-zero, when its timeout ladder killed it
|
|
138
|
+
* (`timed_out: true`), or when the gate itself recorded the run as
|
|
139
|
+
* `orchestrator.quality_gate.failed`. The reporting variants of
|
|
140
|
+
* `scripts/run-quality-gate.mjs` keep `exit_code: 0` on a killed run (#1459),
|
|
141
|
+
* so the exit code alone scored that run green. Both gate dimensions use this
|
|
142
|
+
* one predicate so they can never disagree about the same event.
|
|
143
|
+
*
|
|
144
|
+
* @param {{event?: string, exit_code?: unknown, timed_out?: unknown}} e
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
*/
|
|
147
|
+
export function isFailedGateEvent(e) {
|
|
148
|
+
return e.exit_code !== 0 || e.timed_out === true || e.event === 'orchestrator.quality_gate.failed';
|
|
149
|
+
}
|
|
150
|
+
|
|
127
151
|
function isNonEmptyString(v) {
|
|
128
152
|
return typeof v === 'string' && v.trim().length > 0;
|
|
129
153
|
}
|
|
@@ -223,12 +247,12 @@ function countAttributedEvents(ctx, eventName) {
|
|
|
223
247
|
// ---------------------------------------------------------------------------
|
|
224
248
|
|
|
225
249
|
/**
|
|
226
|
-
* verification-evidence: ≥1 quality_gate event in the clean window ∧
|
|
227
|
-
*
|
|
250
|
+
* verification-evidence: ≥1 quality_gate event in the clean window ∧ none
|
|
251
|
+
* failed (`isFailedGateEvent`) → pass; any failed → fail; 0 events ∧ total_files_changed==0
|
|
228
252
|
* → not-applicable; 0 events otherwise / peer-contaminated window → cannot-determine.
|
|
229
253
|
*
|
|
230
254
|
* Exported ONLY so `tests/eval/rubric-parity.test.mjs` can drive it row-by-row
|
|
231
|
-
* against the pre-registered table in `skills/eval/rubric-
|
|
255
|
+
* against the pre-registered table in `skills/eval/rubric-v3.md`. NOT a public
|
|
232
256
|
* contract: signature, arguments and return shape may change with the rubric.
|
|
233
257
|
*/
|
|
234
258
|
export function scoreVerificationEvidence(ctx) {
|
|
@@ -266,20 +290,20 @@ export function scoreVerificationEvidence(ctx) {
|
|
|
266
290
|
};
|
|
267
291
|
}
|
|
268
292
|
|
|
269
|
-
const failing = gates.filter(
|
|
293
|
+
const failing = gates.filter(isFailedGateEvent);
|
|
270
294
|
if (failing.length === 0) {
|
|
271
295
|
return {
|
|
272
296
|
id,
|
|
273
297
|
method,
|
|
274
298
|
status: 'pass',
|
|
275
|
-
evidence: `attribution: time-window. ${gates.length} quality_gate event(s) in window, all exit_code=0.`,
|
|
299
|
+
evidence: `attribution: time-window. ${gates.length} quality_gate event(s) in window, all exit_code=0, none timed_out or recorded failed.`,
|
|
276
300
|
};
|
|
277
301
|
}
|
|
278
302
|
return {
|
|
279
303
|
id,
|
|
280
304
|
method,
|
|
281
305
|
status: 'fail',
|
|
282
|
-
evidence: `attribution: time-window. ${gates.length} quality_gate event(s) in window; ${failing.length}
|
|
306
|
+
evidence: `attribution: time-window. ${gates.length} quality_gate event(s) in window; ${failing.length} failed (non-zero exit_code, timed_out or quality_gate.failed).`,
|
|
283
307
|
};
|
|
284
308
|
}
|
|
285
309
|
|
|
@@ -290,7 +314,7 @@ export function scoreVerificationEvidence(ctx) {
|
|
|
290
314
|
* work → cannot-determine. score = completion_rate (informative).
|
|
291
315
|
*
|
|
292
316
|
* Exported ONLY so `tests/eval/rubric-parity.test.mjs` can drive it row-by-row
|
|
293
|
-
* against the pre-registered table in `skills/eval/rubric-
|
|
317
|
+
* against the pre-registered table in `skills/eval/rubric-v3.md`. NOT a public
|
|
294
318
|
* contract: signature, arguments and return shape may change with the rubric.
|
|
295
319
|
*/
|
|
296
320
|
export function scorePlanFidelity(ctx) {
|
|
@@ -334,12 +358,13 @@ export function scorePlanFidelity(ctx) {
|
|
|
334
358
|
|
|
335
359
|
/**
|
|
336
360
|
* gate-health: like verification-evidence but ONLY variant=='full-gate' events;
|
|
337
|
-
* pass = the LAST full-gate in the clean window
|
|
361
|
+
* pass = the LAST full-gate in the clean window is not a failed gate event
|
|
362
|
+
* (`isFailedGateEvent`, rubric-v3), else fail.
|
|
338
363
|
* 0 full-gate events → not-applicable when no waves ran (housekeeping), else
|
|
339
364
|
* cannot-determine. Peer-contaminated window → cannot-determine.
|
|
340
365
|
*
|
|
341
366
|
* Exported ONLY so `tests/eval/rubric-parity.test.mjs` can drive it row-by-row
|
|
342
|
-
* against the pre-registered table in `skills/eval/rubric-
|
|
367
|
+
* against the pre-registered table in `skills/eval/rubric-v3.md`. NOT a public
|
|
343
368
|
* contract: signature, arguments and return shape may change with the rubric.
|
|
344
369
|
*/
|
|
345
370
|
export function scoreGateHealth(ctx) {
|
|
@@ -390,12 +415,15 @@ export function scoreGateHealth(ctx) {
|
|
|
390
415
|
const last = fullGates.reduce((a, b) =>
|
|
391
416
|
Date.parse(a.timestamp) >= Date.parse(b.timestamp) ? a : b,
|
|
392
417
|
);
|
|
393
|
-
const
|
|
418
|
+
const lastFailed = isFailedGateEvent(last);
|
|
419
|
+
// A failed run that still exited 0 (killed, or recorded `.failed`) carries a
|
|
420
|
+
// marker, so the judge's `last_full_gate_exit=0` is not read as a green finish.
|
|
421
|
+
const flag = lastFailed && last.exit_code === 0 ? ', last failed-flag=true' : '';
|
|
394
422
|
return {
|
|
395
423
|
id,
|
|
396
424
|
method,
|
|
397
|
-
status,
|
|
398
|
-
evidence: `attribution: time-window. ${fullGates.length} full-gate event(s) in window; last exit_code=${last.exit_code}.`,
|
|
425
|
+
status: lastFailed ? 'fail' : 'pass',
|
|
426
|
+
evidence: `attribution: time-window. ${fullGates.length} full-gate event(s) in window; last exit_code=${last.exit_code}${flag}.`,
|
|
399
427
|
};
|
|
400
428
|
}
|
|
401
429
|
|
|
@@ -420,7 +448,7 @@ export function scoreGateHealth(ctx) {
|
|
|
420
448
|
* those only `spiral` is emitted at all, which the evidence discloses.
|
|
421
449
|
*
|
|
422
450
|
* Exported ONLY so `tests/eval/rubric-parity.test.mjs` can drive it row-by-row
|
|
423
|
-
* against the pre-registered table in `skills/eval/rubric-
|
|
451
|
+
* against the pre-registered table in `skills/eval/rubric-v3.md`. NOT a public
|
|
424
452
|
* contract: signature, arguments and return shape may change with the rubric.
|
|
425
453
|
*/
|
|
426
454
|
export function scoreProcessSafety(ctx) {
|
|
@@ -465,7 +493,7 @@ export function scoreProcessSafety(ctx) {
|
|
|
465
493
|
* window — see `countAttributedEvents`.
|
|
466
494
|
*
|
|
467
495
|
* Exported ONLY so `tests/eval/rubric-parity.test.mjs` can drive it row-by-row
|
|
468
|
-
* against the pre-registered table in `skills/eval/rubric-
|
|
496
|
+
* against the pre-registered table in `skills/eval/rubric-v3.md`. NOT a public
|
|
469
497
|
* contract: signature, arguments and return shape may change with the rubric.
|
|
470
498
|
*/
|
|
471
499
|
export function scoreGuardFriction(ctx) {
|
|
@@ -509,7 +537,7 @@ export function scoreGuardFriction(ctx) {
|
|
|
509
537
|
* Missing values are null ("don't fake perfect"), never guessed.
|
|
510
538
|
*
|
|
511
539
|
* Exported ONLY so `tests/eval/rubric-parity.test.mjs` can drive it row-by-row
|
|
512
|
-
* against the pre-registered table in `skills/eval/rubric-
|
|
540
|
+
* against the pre-registered table in `skills/eval/rubric-v3.md`. NOT a public
|
|
513
541
|
* contract: signature, arguments and return shape may change with the rubric.
|
|
514
542
|
*/
|
|
515
543
|
export function scoreEfficiencyKpis(kpis) {
|
|
@@ -528,7 +556,7 @@ export function scoreEfficiencyKpis(kpis) {
|
|
|
528
556
|
* Dimension id → its scorer, in the canonical `RUBRIC_DIMENSION_IDS` order.
|
|
529
557
|
*
|
|
530
558
|
* Exists so the rubric parity test resolves a scorer from the dimension ANCHOR
|
|
531
|
-
* it parsed out of `skills/eval/rubric-
|
|
559
|
+
* it parsed out of `skills/eval/rubric-v3.md` instead of retyping six function
|
|
532
560
|
* names — a hand-typed list under a census title is a green tick with no cover
|
|
533
561
|
* (`.claude/rules/measurement-discipline.md`). NOT a public contract; the
|
|
534
562
|
* callable values are the same test-only exports as above.
|
|
@@ -579,7 +607,7 @@ const EVENT_DERIVED_DIMENSIONS = new Set([
|
|
|
579
607
|
* turns a partial result into a clean verdict.
|
|
580
608
|
*
|
|
581
609
|
* NOT A RUBRIC CHANGE — and deliberately so. This appends to `evidence` and
|
|
582
|
-
* touches no `status`: no pre-registered formula in `skills/eval/rubric-
|
|
610
|
+
* touches no `status`: no pre-registered formula in `skills/eval/rubric-v3.md`
|
|
583
611
|
* reads it, and no dimension can flip verdict because of it. Measured
|
|
584
612
|
* 2026-09-20 over the 40 records in `.orchestrator/metrics/eval.jsonl`: the
|
|
585
613
|
* live ledger reads `complete: true`, `gaps: []`, `malformed_lines: 0`
|
|
@@ -652,8 +680,11 @@ export const EVIDENCE_PATTERNS = Object.freeze({
|
|
|
652
680
|
gateRunsTotal: /(\d+) quality_gate event\(s\) in window/,
|
|
653
681
|
/** ≥1-gate, all-green branch. */
|
|
654
682
|
gateAllGreen: /quality_gate event\(s\) in window, all exit_code=0/,
|
|
655
|
-
/**
|
|
656
|
-
|
|
683
|
+
/**
|
|
684
|
+
* ≥1-gate, some-red branch — rubric-v3 "…; N failed (non-zero exit_code, …)."
|
|
685
|
+
* and, for stored rubric-v2 records, "…; N with non-zero exit_code.".
|
|
686
|
+
*/
|
|
687
|
+
gateRunsFailed: /(\d+) (?:failed \(non-zero exit_code|with non-zero exit_code)/,
|
|
657
688
|
/** 0-gate, nothing changed → not-applicable branch. */
|
|
658
689
|
noChangeToVerify: /0 quality_gate events in window and total_files_changed=0/,
|
|
659
690
|
/** 0-gate, files changed → cannot-determine branch. */
|
|
@@ -673,6 +704,8 @@ export const EVIDENCE_PATTERNS = Object.freeze({
|
|
|
673
704
|
/** ≥1-full-gate branch ("N full-gate event(s) in window; last exit_code=X."). */
|
|
674
705
|
fullGateRuns: /(\d+) full-gate event\(s\) in window/,
|
|
675
706
|
lastFullGateExit: /last exit_code=(-?\d+)/,
|
|
707
|
+
/** rubric-v3: the last full-gate failed although it exited 0 (killed / recorded `.failed`). */
|
|
708
|
+
lastFullGateFailedFlag: /last failed-flag=true/,
|
|
676
709
|
/** 0-full-gate branches (not-applicable / cannot-determine). */
|
|
677
710
|
fullGateZero: /0 full-gate events (?:in window|and no waves ran)/,
|
|
678
711
|
}),
|
|
@@ -690,7 +723,7 @@ export const EVIDENCE_PATTERNS = Object.freeze({
|
|
|
690
723
|
// `RecordFacts` field and no rubric-v2 decision rule names them, so a
|
|
691
724
|
// pattern for them would be a reader with zero consumers (BV-001.1). They
|
|
692
725
|
// were exactly that until 2026-09-19. Adding one back means adding the
|
|
693
|
-
// fact to the pre-registered list in `skills/eval/rubric-
|
|
726
|
+
// fact to the pre-registered list in `skills/eval/rubric-v3.md` first.
|
|
694
727
|
/** attribution marker: session-id (preferred) vs the time-window fallback. */
|
|
695
728
|
attributionSessionId: /attribution: session-id/,
|
|
696
729
|
attributionTimeWindow: /attribution: time-window/,
|
|
@@ -854,7 +887,7 @@ function resolveModel(model, env, resolveModelFromEnv) {
|
|
|
854
887
|
export const REQUIRED_EVENTS_WINDOW_DAYS = null;
|
|
855
888
|
|
|
856
889
|
/**
|
|
857
|
-
* Evaluate one completed session against the rubric-
|
|
890
|
+
* Evaluate one completed session against the rubric-v3 dimensions.
|
|
858
891
|
*
|
|
859
892
|
* @param {object} opts
|
|
860
893
|
* @param {string} [opts.sessionId] — explicit session_id; default is the cascade.
|
|
@@ -899,7 +932,8 @@ export function evaluateSession(opts = {}) {
|
|
|
899
932
|
}
|
|
900
933
|
|
|
901
934
|
const sessionsPath = path.join(metricsDir, 'sessions.jsonl');
|
|
902
|
-
|
|
935
|
+
// #1514 point 1 — the repo's ONE ledger (main checkout for a linked worktree).
|
|
936
|
+
const eventsPath = path.join(resolveEventsLedgerRoot(metricsDir), 'events.jsonl');
|
|
903
937
|
// #1209: sessions.jsonl is APPEND-ONLY (the same physical session can carry
|
|
904
938
|
// more than one line — crash-recovery re-appends, #1068 stub/supersede
|
|
905
939
|
// pairs), so a raw readJsonlFile() left resolveSession()/findPeerOverlap()
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* eval/judge.mjs — opt-in advisory LLM-judge overlay for the aiat-llm-eval
|
|
3
3
|
* standard (Epic #803, S7 / issue #810).
|
|
4
4
|
*
|
|
5
|
-
* Overlays the ONE pre-registered judge dimension from `skills/eval/rubric-
|
|
5
|
+
* Overlays the ONE pre-registered judge dimension from `skills/eval/rubric-v3.md`
|
|
6
6
|
* § "Judge Dimensions" — `instruction-adherence` — onto a deterministic
|
|
7
7
|
* session-eval record produced by `scripts/lib/eval/engine.mjs`.
|
|
8
8
|
* Default OFF (`eval.judge: off` in Session Config); when disabled, zero code in
|
|
@@ -64,7 +64,7 @@ export const DEFAULT_BUDGET = Object.freeze({ input: 8000, output: 4000 });
|
|
|
64
64
|
const CHARS_PER_TOKEN = 4;
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* The pre-registered judge dimension ids (rubric-
|
|
67
|
+
* The pre-registered judge dimension ids (rubric-v3.md § "Judge Dimensions").
|
|
68
68
|
* Fixed set — the judge may never invent a second dimension. `report-quality`
|
|
69
69
|
* was retired in v2 (#1381; see the module docblock for the two measurements).
|
|
70
70
|
*/
|
|
@@ -82,11 +82,11 @@ export const JUDGE_DIMENSION_IDS = Object.freeze(['instruction-adherence']);
|
|
|
82
82
|
* signal that is allowed to speak at all. Numerator AND denominator are quoted
|
|
83
83
|
* here because the bare percentage disagreed with its own rubric copy: this
|
|
84
84
|
* docblock read 5.1% (= 2/39) against the rubric's 5.0% for the same
|
|
85
|
-
* measurement. Pre-registered in `skills/eval/rubric-
|
|
85
|
+
* measurement. Pre-registered in `skills/eval/rubric-v3.md`.
|
|
86
86
|
*/
|
|
87
87
|
export const GUARD_BLOCKED_CONSPICUOUS_THRESHOLD = 20;
|
|
88
88
|
|
|
89
|
-
/** The judge question text per dimension, pre-registered verbatim in rubric-
|
|
89
|
+
/** The judge question text per dimension, pre-registered verbatim in rubric-v3.md. */
|
|
90
90
|
export const JUDGE_QUESTIONS = Object.freeze({
|
|
91
91
|
'instruction-adherence':
|
|
92
92
|
"Reading the session-eval record's dimension evidence, kpis, session_id and the pre-computed facts below, did the coordinator follow the operator's stated instructions and the repo's always-on rules (verification-before-completion, ask-via-tool, parallel-session safety, scope discipline) — or is a concrete deviation visible in the record?",
|
|
@@ -94,7 +94,7 @@ export const JUDGE_QUESTIONS = Object.freeze({
|
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
96
|
* The ordered decision rules for `instruction-adherence`, pre-registered
|
|
97
|
-
* verbatim in `skills/eval/rubric-
|
|
97
|
+
* verbatim in `skills/eval/rubric-v3.md` § "Judge Dimensions". Applied IN THIS
|
|
98
98
|
* ORDER; the first rule that applies decides.
|
|
99
99
|
*
|
|
100
100
|
* They exist because the v1 wording was under-specified: it named "isolated
|
|
@@ -213,7 +213,7 @@ const LEGACY_V1_SPIRAL = /(?:^|[\s(])(\d+) spiral\b/;
|
|
|
213
213
|
/**
|
|
214
214
|
* @typedef {Object} RecordFacts
|
|
215
215
|
* @property {number|null} gate_runs_total quality_gate events attributed to the session
|
|
216
|
-
* @property {number|null} gate_runs_failed of those, how many
|
|
216
|
+
* @property {number|null} gate_runs_failed of those, how many failed (rubric-v3: non-zero exit, timed_out, or `.failed`)
|
|
217
217
|
* @property {number|null} full_gate_runs full-gate events attributed to the session
|
|
218
218
|
* @property {number|null} last_full_gate_exit exit code of the LAST full gate
|
|
219
219
|
* @property {boolean|null} red_runs_then_green_finish red intermediate runs, green finish
|
|
@@ -356,6 +356,10 @@ export function computeRecordFacts(dimensions) {
|
|
|
356
356
|
const ghEv = ev('gate-health');
|
|
357
357
|
let full_gate_runs = null;
|
|
358
358
|
let last_full_gate_exit = null;
|
|
359
|
+
// rubric-v3 (#1487): the last full-gate can be red while it exited 0 (killed
|
|
360
|
+
// reporting run). Not a fact of its own — it only keeps the exit code from
|
|
361
|
+
// being read as a green finish below.
|
|
362
|
+
let last_full_gate_flagged_failed = false;
|
|
359
363
|
if (ghEv !== null) {
|
|
360
364
|
if (GH.windowContaminated.test(ghEv)) {
|
|
361
365
|
// Same construction as above — unattributable, no number to read.
|
|
@@ -365,6 +369,7 @@ export function computeRecordFacts(dimensions) {
|
|
|
365
369
|
} else if (GH.fullGateRuns.test(ghEv)) {
|
|
366
370
|
full_gate_runs = num(ghEv, GH.fullGateRuns);
|
|
367
371
|
last_full_gate_exit = num(ghEv, GH.lastFullGateExit);
|
|
372
|
+
last_full_gate_flagged_failed = GH.lastFullGateFailedFlag.test(ghEv);
|
|
368
373
|
if (last_full_gate_exit === null) {
|
|
369
374
|
miss('last_full_gate_exit', 'gate-health', 'the ≥1-full-gate branch matched but carried no exit code');
|
|
370
375
|
}
|
|
@@ -454,10 +459,11 @@ export function computeRecordFacts(dimensions) {
|
|
|
454
459
|
const carryover = planCarryover ?? kpiCarryover;
|
|
455
460
|
|
|
456
461
|
// --- derived + contradictions -------------------------------------------
|
|
462
|
+
const lastFullGateGreen = last_full_gate_exit === 0 && !last_full_gate_flagged_failed;
|
|
457
463
|
const red_runs_then_green_finish =
|
|
458
464
|
gate_runs_failed === null || last_full_gate_exit === null
|
|
459
465
|
? null
|
|
460
|
-
: gate_runs_failed > 0 &&
|
|
466
|
+
: gate_runs_failed > 0 && lastFullGateGreen;
|
|
461
467
|
|
|
462
468
|
let window_contaminated = null;
|
|
463
469
|
const contaminationSources = [
|
|
@@ -484,7 +490,7 @@ export function computeRecordFacts(dimensions) {
|
|
|
484
490
|
if (st('gate-health') === 'pass' && last_full_gate_exit !== null && last_full_gate_exit !== 0) {
|
|
485
491
|
contradictions.push(`gate-health status=pass but last_full_gate_exit=${last_full_gate_exit}`);
|
|
486
492
|
}
|
|
487
|
-
if (st('gate-health') === 'fail' &&
|
|
493
|
+
if (st('gate-health') === 'fail' && lastFullGateGreen) {
|
|
488
494
|
contradictions.push('gate-health status=fail but last_full_gate_exit=0');
|
|
489
495
|
}
|
|
490
496
|
if (planCarryover !== null && kpiCarryover !== null && planCarryover !== kpiCarryover) {
|
|
@@ -10,8 +10,13 @@
|
|
|
10
10
|
* order) that is either status:'completed' or non-abandoned with completed
|
|
11
11
|
* work. Abandoned records are ALWAYS skipped.
|
|
12
12
|
*
|
|
13
|
-
* 2. WHOSE events? — quality_gate events in events.jsonl
|
|
14
|
-
*
|
|
13
|
+
* 2. WHOSE events? — quality_gate events in events.jsonl do not RELIABLY
|
|
14
|
+
* carry a session_id (measured 2026-10-05, this repo's events.jsonl:
|
|
15
|
+
* 17 of 82 `orchestrator.quality_gate.*` records without one, 65 with —
|
|
16
|
+
* `grep '"orchestrator.quality_gate' <file> | grep -c '"session_id"'`;
|
|
17
|
+
* the id-less ones are interleaved through the file, not just the oldest),
|
|
18
|
+
* so this module attributes them to a
|
|
19
|
+
* session by its wall-clock window
|
|
15
20
|
* [started_at, completed_at] (Decision #1: attribution = time-window).
|
|
16
21
|
* findPeerOverlap() detects when that window overlaps ANY other session's
|
|
17
22
|
* window — a contaminated window means gate-attribution is unsafe and the
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
import { closeSync, existsSync, openSync, readSync, readdirSync } from 'node:fs';
|
|
89
89
|
import path from 'node:path';
|
|
90
90
|
|
|
91
|
+
import { eventsFilePath } from './events.mjs';
|
|
91
92
|
import { ARCHIVE_DIR_NAME, ARCHIVE_NAME_RE, LEGACY_RING_MAX, ROTATION_EVENT } from './events-schema.mjs';
|
|
92
93
|
|
|
93
94
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
@@ -268,9 +269,11 @@ async function maxRequiredWindow(readers) {
|
|
|
268
269
|
*/
|
|
269
270
|
export async function checkEventsRetention({ repoRoot, config, now = Date.now() } = {}) {
|
|
270
271
|
if (!repoRoot || typeof repoRoot !== 'string') return null;
|
|
271
|
-
|
|
272
|
+
// #1514 point 1 — the repo's ONE ledger; its archive dir sits beside the SAME
|
|
273
|
+
// resolved file rotation writes (`events-rotation.mjs`: `<dirname(log)>/_archive`).
|
|
274
|
+
const activePath = eventsFilePath(repoRoot);
|
|
275
|
+
const metricsDir = path.dirname(activePath);
|
|
272
276
|
const archiveDir = path.join(metricsDir, ARCHIVE_DIR_NAME);
|
|
273
|
-
const activePath = path.join(metricsDir, 'events.jsonl');
|
|
274
277
|
const nowMs = typeof now === 'number' && Number.isFinite(now) ? now : Date.now();
|
|
275
278
|
|
|
276
279
|
let names;
|
package/scripts/lib/events.mjs
CHANGED
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
} from 'node:fs';
|
|
53
53
|
import { tmpdir } from 'node:os';
|
|
54
54
|
import path from 'node:path';
|
|
55
|
-
import { getProjectDir, SO_SHARED_DIR } from './platform.mjs';
|
|
55
|
+
import { getProjectDir, resolveEventsLedgerRoot, SO_SHARED_DIR } from './platform.mjs';
|
|
56
56
|
import { readLock } from './session-lock.mjs';
|
|
57
57
|
import { resolveStateMdPath } from './state-md/frontmatter-mutators.mjs';
|
|
58
58
|
import {
|
|
@@ -139,8 +139,12 @@ function sandboxedDefault(defaultPath) {
|
|
|
139
139
|
/**
|
|
140
140
|
* Returns the absolute path to `.orchestrator/metrics/events.jsonl` under `repoRoot`.
|
|
141
141
|
*
|
|
142
|
-
* `repoRoot` defaults to the
|
|
143
|
-
*
|
|
142
|
+
* `repoRoot` defaults to the ONE events-ledger root, `resolveEventsLedgerRoot()`
|
|
143
|
+
* (`platform.mjs`, #1514 point 1): the launch dir from env, else the main
|
|
144
|
+
* checkout when the cwd is a linked git worktree, else `getProjectDir()` — the
|
|
145
|
+
* pre-#1514 default, unchanged everywhere a worktree is not involved (#941). An
|
|
146
|
+
* explicit absolute `repoRoot` inside a linked worktree is mapped to its main
|
|
147
|
+
* checkout the same way. Pass an explicit
|
|
144
148
|
* `repoRoot` when the destination must be pinned to a tree other than the
|
|
145
149
|
* CWD/env-resolved project — e.g. a unit test running the gate against a tmp
|
|
146
150
|
* repo, which must NOT append synthetic records to the real fleet telemetry.
|
|
@@ -154,8 +158,18 @@ function sandboxedDefault(defaultPath) {
|
|
|
154
158
|
* @returns {string}
|
|
155
159
|
*/
|
|
156
160
|
export function eventsFilePath(repoRoot) {
|
|
157
|
-
if (repoRoot !== undefined)
|
|
158
|
-
|
|
161
|
+
if (repoRoot !== undefined) {
|
|
162
|
+
// #1514 point 1 — an absolute root inside a LINKED git worktree maps to the
|
|
163
|
+
// same spot in its main checkout: one ledger per repo, never one per
|
|
164
|
+
// worktree. Identity for every other root (a plain repo, a tmp fixture, a
|
|
165
|
+
// non-git dir), and a relative/empty value is joined exactly as before —
|
|
166
|
+
// `emitEvent()` refuses it upstream.
|
|
167
|
+
const root = typeof repoRoot === 'string' && path.isAbsolute(repoRoot)
|
|
168
|
+
? resolveEventsLedgerRoot(repoRoot)
|
|
169
|
+
: repoRoot;
|
|
170
|
+
return path.join(root, SO_SHARED_DIR, 'metrics', 'events.jsonl');
|
|
171
|
+
}
|
|
172
|
+
return sandboxedDefault(path.join(resolveEventsLedgerRoot(), SO_SHARED_DIR, 'metrics', 'events.jsonl'));
|
|
159
173
|
}
|
|
160
174
|
|
|
161
175
|
/**
|
|
@@ -578,6 +592,60 @@ function discoverArchives(logPath) {
|
|
|
578
592
|
return { archives, legacy: present.map((i) => `${logPath}.${i}`), ringHoles, unindexed };
|
|
579
593
|
}
|
|
580
594
|
|
|
595
|
+
/**
|
|
596
|
+
* The one tombstone-to-archive match (#1505 5c) — shared by
|
|
597
|
+
* {@link readEventsWithRotations} rule 1, {@link scanEventsBackwards}, and
|
|
598
|
+
* `planWithGaps` in `scripts/backfill-abandoned-sessions.mjs`, which used to
|
|
599
|
+
* carry three hand-kept copies of it.
|
|
600
|
+
*
|
|
601
|
+
* #1411 — the tombstone resolves against THIS ledger's own `_archive/`, by
|
|
602
|
+
* BASENAME, never against the absolute value it stores. The writer keeps
|
|
603
|
+
* `archived_as` absolute (`events-rotation.mjs` joins the repo root) and that
|
|
604
|
+
* value stays in the record as PROVENANCE. It is not a lookup key: a moved
|
|
605
|
+
* checkout, a clone, or a sibling git worktree — routine here — makes every
|
|
606
|
+
* tombstone name a path that does not exist in THIS tree, so an exact
|
|
607
|
+
* comparison reported a phantom `missing-archive` for an archive sitting right
|
|
608
|
+
* beside the active file. Basename, not `realpath`: realpath cannot resolve a
|
|
609
|
+
* path that no longer exists, which IS the failure mode.
|
|
610
|
+
*
|
|
611
|
+
* ORDER (why the absolute value has no second chance): the sibling answer is
|
|
612
|
+
* consulted first, and an absolute hit would only be trustworthy while it
|
|
613
|
+
* pointed INSIDE this ledger's own archive dir — but any such path resolves to
|
|
614
|
+
* exactly the sibling path already tested, so once the sibling misses, an
|
|
615
|
+
* absolute hit can ONLY be a still-present OLD checkout. Honouring it would
|
|
616
|
+
* validate THIS ledger against a FOREIGN repo's archive: a silent false
|
|
617
|
+
* negative, worse than the phantom gap.
|
|
618
|
+
*
|
|
619
|
+
* #1423 — THREE outcomes, not two: read (`null`), present-but-unread
|
|
620
|
+
* (`unindexed-archive`), gone (`missing-archive`). The middle one used to be
|
|
621
|
+
* folded into the first by `existsSync`, so a tombstone whose archive had been
|
|
622
|
+
* RENAMED out of `ARCHIVE_NAME_RE` reported `complete: true` while its records
|
|
623
|
+
* were absent.
|
|
624
|
+
*
|
|
625
|
+
* @param {unknown} record a parsed ledger line
|
|
626
|
+
* @param {string} ownArchiveDir this ledger's `_archive/`, built exactly as
|
|
627
|
+
* `discoverArchives` builds it so a read archive matches `readSet` by string
|
|
628
|
+
* @param {Set<string>} readSet the paths this read actually took as sources
|
|
629
|
+
* @returns {{kind: 'unindexed-archive'|'missing-archive', path: string,
|
|
630
|
+
* archived_as: string, first_ts: string|null, last_ts: string|null}|null}
|
|
631
|
+
* `null` when `record` is not a rotation tombstone, names no archive, or its
|
|
632
|
+
* archive was read.
|
|
633
|
+
*/
|
|
634
|
+
export function tombstoneGap(record, ownArchiveDir, readSet) {
|
|
635
|
+
if (record?.event !== ROTATION_EVENT) return null;
|
|
636
|
+
const target = record.archived_as;
|
|
637
|
+
if (typeof target !== 'string' || target.length === 0) return null;
|
|
638
|
+
const sibling = path.join(ownArchiveDir, path.basename(target));
|
|
639
|
+
if (readSet.has(sibling)) return null;
|
|
640
|
+
return {
|
|
641
|
+
kind: existsSync(sibling) ? 'unindexed-archive' : 'missing-archive',
|
|
642
|
+
path: sibling,
|
|
643
|
+
archived_as: target,
|
|
644
|
+
first_ts: record.first_ts ?? null,
|
|
645
|
+
last_ts: record.last_ts ?? null,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
581
649
|
/**
|
|
582
650
|
* Read the events ledger ACROSS rotation boundaries — active file plus every
|
|
583
651
|
* archive still on disk — in time order, reporting what is missing instead of
|
|
@@ -660,44 +728,14 @@ export function readEventsWithRotations(repoRoot, opts = {}) {
|
|
|
660
728
|
// `onDisk` still match by string.
|
|
661
729
|
const ownArchiveDir = path.join(path.dirname(activePath), ARCHIVE_DIR_NAME);
|
|
662
730
|
|
|
663
|
-
// Rule 1 — every rotation tombstone must still point at a file
|
|
731
|
+
// Rule 1 — every rotation tombstone must still point at a file (resolution
|
|
732
|
+
// and the three outcomes: {@link tombstoneGap}).
|
|
664
733
|
for (const source of sources) {
|
|
665
734
|
for (const record of source.records) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if (typeof target !== 'string' || target.length === 0) continue;
|
|
669
|
-
// #1411 — resolve the tombstone against THIS ledger's own `_archive/`,
|
|
670
|
-
// by BASENAME, and never against the absolute value it stores.
|
|
671
|
-
//
|
|
672
|
-
// The writer keeps `archived_as` absolute (`events-rotation.mjs` joins
|
|
673
|
-
// the repo root) and that value stays in the record as PROVENANCE. It is
|
|
674
|
-
// not a lookup key: a moved checkout, a clone, or a sibling git worktree
|
|
675
|
-
// — routine here — makes every tombstone name a path that does not exist
|
|
676
|
-
// in THIS tree, so the old exact comparison reported a phantom
|
|
677
|
-
// `missing-archive` for an archive sitting right beside the active file.
|
|
678
|
-
// Basename, not `realpath`: realpath cannot resolve a path that no longer
|
|
679
|
-
// exists, which IS the failure mode.
|
|
680
|
-
//
|
|
681
|
-
// ORDER (why the absolute value has no second chance): the sibling answer
|
|
682
|
-
// is consulted first, and an absolute hit would only be trustworthy while
|
|
683
|
-
// it pointed INSIDE this ledger's own archive dir — but any such path
|
|
684
|
-
// resolves to exactly the sibling path already tested, so once the
|
|
685
|
-
// sibling misses, an absolute hit can ONLY be a still-present OLD
|
|
686
|
-
// checkout. Honouring it would validate THIS ledger against a FOREIGN
|
|
687
|
-
// repo's archive: a silent false negative, worse than the phantom gap.
|
|
688
|
-
const sibling = path.join(ownArchiveDir, path.basename(target));
|
|
689
|
-
if (onDisk.has(sibling)) continue;
|
|
735
|
+
const gap = tombstoneGap(record, ownArchiveDir, onDisk);
|
|
736
|
+
if (gap === null) continue;
|
|
690
737
|
gaps.push({
|
|
691
|
-
|
|
692
|
-
// (here), gone (below). The middle one used to be silently folded into
|
|
693
|
-
// the first by `existsSync`, so a tombstone whose archive had been
|
|
694
|
-
// RENAMED out of `ARCHIVE_NAME_RE` reported `complete: true` while its
|
|
695
|
-
// records were absent from `events`.
|
|
696
|
-
kind: existsSync(sibling) ? 'unindexed-archive' : 'missing-archive',
|
|
697
|
-
path: sibling,
|
|
698
|
-
archived_as: target,
|
|
699
|
-
first_ts: record.first_ts ?? null,
|
|
700
|
-
last_ts: record.last_ts ?? null,
|
|
738
|
+
...gap,
|
|
701
739
|
lines: record.lines ?? null,
|
|
702
740
|
size_before: record.size_before ?? null,
|
|
703
741
|
reported_by: source.path,
|
|
@@ -866,8 +904,7 @@ export function scanEventsBackwards(opts = {}) {
|
|
|
866
904
|
* LOW-2): `maybeRotate` renames, prunes, THEN appends the tombstone, from an
|
|
867
905
|
* async SessionStart hook, so a concurrent append can land first and push it
|
|
868
906
|
* to line 2. Called BEFORE the caller's `filter`, which names another event
|
|
869
|
-
* and would otherwise hide the cut.
|
|
870
|
-
* ledger's own `_archive/`, never by the absolute provenance value (#1411).
|
|
907
|
+
* and would otherwise hide the cut. Matched by {@link tombstoneGap}.
|
|
871
908
|
*/
|
|
872
909
|
const noteCut = (line, source) => {
|
|
873
910
|
if (!line.includes(ROTATION_EVENT)) return;
|
|
@@ -877,18 +914,8 @@ export function scanEventsBackwards(opts = {}) {
|
|
|
877
914
|
} catch {
|
|
878
915
|
return; // counted by `consume` like every other malformed line
|
|
879
916
|
}
|
|
880
|
-
const
|
|
881
|
-
if (
|
|
882
|
-
const sibling = path.join(ownArchiveDir, path.basename(target));
|
|
883
|
-
if (listed.has(sibling)) return;
|
|
884
|
-
gaps.push({
|
|
885
|
-
kind: existsSync(sibling) ? 'unindexed-archive' : 'missing-archive',
|
|
886
|
-
path: sibling,
|
|
887
|
-
archived_as: target,
|
|
888
|
-
first_ts: record.first_ts ?? null,
|
|
889
|
-
last_ts: record.last_ts ?? null,
|
|
890
|
-
reported_by: source.path,
|
|
891
|
-
});
|
|
917
|
+
const gap = tombstoneGap(record, ownArchiveDir, listed);
|
|
918
|
+
if (gap !== null) gaps.push({ ...gap, reported_by: source.path });
|
|
892
919
|
};
|
|
893
920
|
|
|
894
921
|
/** @returns {boolean} true ⇒ the caller accepted a record; stop everything. */
|