mandrel 2.24.0 → 2.26.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/agents/acceptance-critic.md +19 -21
- package/.agents/agents/auditor.md +16 -19
- package/.agents/agents/story-worker.md +41 -54
- package/.agents/audit-checklists/architecture.md +5 -1
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +1 -1
- package/.agents/rules/ci-remediation.md +68 -3
- package/.agents/schemas/agentrc.schema.json +6 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +8 -0
- package/.agents/schemas/baselines/crap.schema.json +13 -1
- package/.agents/schemas/crap-report.schema.json +37 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +124 -15
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/audit-labels-bootstrap.js +16 -95
- package/.agents/scripts/audit-to-stories.js +33 -3
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-context-budget.js +95 -14
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +121 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
- package/.agents/scripts/deliver-light.js +3 -4
- package/.agents/scripts/deliver-recover.js +13 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
- package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
- package/.agents/scripts/lib/baseline-loader.js +0 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
- package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
- package/.agents/scripts/lib/baselines/reader.js +10 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -7
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
- package/.agents/scripts/lib/crap-engine.js +124 -27
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +86 -13
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/env-loader.js +46 -16
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/findings/route-finding.js +123 -17
- package/.agents/scripts/lib/findings/severity.js +80 -2
- package/.agents/scripts/lib/git/cached-fetch.js +0 -0
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/label-constants.js +17 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
- package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
- package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
- package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +216 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/reserved-test-ids.js +77 -0
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/story-plan.js +137 -42
- package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/util/poll-loop.js +18 -2
- package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
- package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
- package/.agents/scripts/lib/workers/crap-worker.js +8 -13
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/pr-watch-with-update.js +568 -213
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +137 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +36 -36
- package/.agents/scripts/single-story-close.js +34 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/single-story-init.js +7 -0
- package/.agents/scripts/stories-wave-tick.js +308 -47
- package/.agents/scripts/story-plan.js +65 -9
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-crap-baseline.js +42 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/update-maintainability-baseline.js +31 -4
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/templates/single-story-body.md +16 -8
- package/.agents/workflows/audit-architecture.md +69 -13
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/audit-quality.md +26 -0
- package/.agents/workflows/audit-to-stories.md +30 -4
- package/.agents/workflows/deliver.md +92 -97
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/audit-lens-core.md +25 -4
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +95 -4
- package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
- package/.agents/workflows/helpers/deliver-story.md +104 -119
- package/.agents/workflows/helpers/plan-reference.md +49 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +62 -75
- package/README.md +13 -6
- package/docs/CHANGELOG.md +93 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* import-graph.js — the shared static-import graph seam (Story #4902).
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from `check-arch-cycles.js`, which owned the only
|
|
5
|
+
* import-graph builder in the repository and kept it private to its own
|
|
6
|
+
* cycle ratchet. A second consumer now needs the same graph for a very
|
|
7
|
+
* different question — `audit-baselines.js` ranks hotspot files by import
|
|
8
|
+
* in-degree — and re-deriving "which module imports which" a second time
|
|
9
|
+
* would guarantee the two answers drift.
|
|
10
|
+
*
|
|
11
|
+
* Note this is a **module** graph, not the task/DAG graph in `lib/Graph.js`;
|
|
12
|
+
* the two are unrelated despite the shared word.
|
|
13
|
+
*
|
|
14
|
+
* The extraction is behaviour-preserving: `check-arch-cycles.js` imports
|
|
15
|
+
* these helpers and re-exports them, so its public surface (and the ratchet's
|
|
16
|
+
* output) is unchanged.
|
|
17
|
+
*
|
|
18
|
+
* @module lib/import-graph
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default scan roots making up the project's distributed surface — the
|
|
26
|
+
* directories published to npm via `package.json` `files[]`. Resolving
|
|
27
|
+
* them into one graph (relativized against the repo root) means a cycle
|
|
28
|
+
* crossing two roots is visible to consumers of the graph.
|
|
29
|
+
*
|
|
30
|
+
* @type {string[]}
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_ROOTS = [path.join('.agents', 'scripts'), 'bin', 'lib'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Recursively collect `.js` files under `rootDir`, skipping
|
|
36
|
+
* `node_modules`. Returns absolute paths, sorted for determinism.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} rootDir
|
|
39
|
+
* @returns {string[]}
|
|
40
|
+
*/
|
|
41
|
+
export function collectJsFiles(rootDir) {
|
|
42
|
+
const out = [];
|
|
43
|
+
const walk = (dir) => {
|
|
44
|
+
let entries;
|
|
45
|
+
try {
|
|
46
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
47
|
+
} catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (entry.name === 'node_modules') continue;
|
|
52
|
+
const full = path.join(dir, entry.name);
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
walk(full);
|
|
55
|
+
} else if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
56
|
+
out.push(full);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
walk(rootDir);
|
|
61
|
+
return out.sort();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const IMPORT_RE = /from\s+['"](\.\.?\/[^'"]+\.js)['"]/g;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Pure helper: extract relative static-import specifiers from source text.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} source
|
|
70
|
+
* @returns {string[]}
|
|
71
|
+
*/
|
|
72
|
+
export function parseRelativeImports(source) {
|
|
73
|
+
const specs = [];
|
|
74
|
+
for (const m of source.matchAll(IMPORT_RE)) {
|
|
75
|
+
specs.push(m[1]);
|
|
76
|
+
}
|
|
77
|
+
return specs;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build a directed import graph over the given files. Node identity is the
|
|
82
|
+
* file path relative to `rootDir`, posix-separated, so the graph (and any
|
|
83
|
+
* cycles found in it) serializes identically across platforms. Edges that
|
|
84
|
+
* resolve outside the scanned file set are dropped.
|
|
85
|
+
*
|
|
86
|
+
* @param {string[]} files absolute paths
|
|
87
|
+
* @param {string} rootDir
|
|
88
|
+
* @param {{ readFile?: (p: string) => string }} [opts]
|
|
89
|
+
* @returns {Map<string, string[]>}
|
|
90
|
+
*/
|
|
91
|
+
export function buildGraph(files, rootDir, { readFile } = {}) {
|
|
92
|
+
const read = readFile ?? ((p) => fs.readFileSync(p, 'utf-8'));
|
|
93
|
+
const toId = (abs) => path.relative(rootDir, abs).split(path.sep).join('/');
|
|
94
|
+
const idSet = new Set(files.map(toId));
|
|
95
|
+
const graph = new Map();
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
const id = toId(file);
|
|
98
|
+
let source;
|
|
99
|
+
try {
|
|
100
|
+
source = read(file);
|
|
101
|
+
} catch {
|
|
102
|
+
graph.set(id, []);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const edges = [];
|
|
106
|
+
for (const spec of parseRelativeImports(source)) {
|
|
107
|
+
const target = path
|
|
108
|
+
.relative(rootDir, path.resolve(path.dirname(file), spec))
|
|
109
|
+
.split(path.sep)
|
|
110
|
+
.join('/');
|
|
111
|
+
if (idSet.has(target) && target !== id) edges.push(target);
|
|
112
|
+
}
|
|
113
|
+
graph.set(id, [...new Set(edges)].sort());
|
|
114
|
+
}
|
|
115
|
+
return graph;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build the whole-repository import graph by scanning the roots that exist
|
|
120
|
+
* under `cwd`. Returns `null` when none of the roots is present — the
|
|
121
|
+
* "no resolvable import graph" degradation every consumer must tolerate
|
|
122
|
+
* rather than treating an absent graph as a graph with no edges.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} cwd repository root the ids are relativized against
|
|
125
|
+
* @param {{ roots?: string[] }} [opts]
|
|
126
|
+
* @returns {Map<string, string[]> | null}
|
|
127
|
+
*/
|
|
128
|
+
export function resolveRepoGraph(cwd, { roots = DEFAULT_ROOTS } = {}) {
|
|
129
|
+
const present = roots
|
|
130
|
+
.map((dir) => path.resolve(cwd, dir))
|
|
131
|
+
.filter((dir) => fs.existsSync(dir));
|
|
132
|
+
if (present.length === 0) return null;
|
|
133
|
+
const files = present.flatMap((dir) => collectJsFiles(dir));
|
|
134
|
+
if (files.length === 0) return null;
|
|
135
|
+
return buildGraph(files, path.resolve(cwd));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Count inbound edges per node. Nodes with no inbound edge are present in
|
|
140
|
+
* the result with a count of 0, so callers never have to distinguish
|
|
141
|
+
* "unknown module" from "module nothing imports".
|
|
142
|
+
*
|
|
143
|
+
* @param {Map<string, string[]> | null} graph
|
|
144
|
+
* @returns {Map<string, number>} empty when `graph` is null
|
|
145
|
+
*/
|
|
146
|
+
export function computeInDegree(graph) {
|
|
147
|
+
const degrees = new Map();
|
|
148
|
+
if (!graph) return degrees;
|
|
149
|
+
for (const node of graph.keys()) degrees.set(node, 0);
|
|
150
|
+
for (const edges of graph.values()) {
|
|
151
|
+
for (const target of edges) {
|
|
152
|
+
degrees.set(target, (degrees.get(target) ?? 0) + 1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return degrees;
|
|
156
|
+
}
|
|
@@ -111,6 +111,22 @@ export const META_LABELS = {
|
|
|
111
111
|
CONSUMER_IMPROVEMENT: 'meta::consumer-improvement',
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Risk-axis labels. Planning/audit metadata only — `risk::high` carries no
|
|
116
|
+
* runtime behaviour (the single runtime pause point is `agent::blocked`, see
|
|
117
|
+
* `.agents/instructions.md` § 1.J); it marks a ticket a human should look at
|
|
118
|
+
* first. Applied by the audit filer to a Story whose merge contains a Critical
|
|
119
|
+
* finding.
|
|
120
|
+
*
|
|
121
|
+
* Named here because it was a bare `'risk::high'` string literal in
|
|
122
|
+
* `audit-to-stories/build-story-body.js` and defined by no taxonomy at all —
|
|
123
|
+
* neither `LABEL_TAXONOMY` nor the audit bootstrap created it, so the audit
|
|
124
|
+
* filer generated a label the repo did not have (Story #4877).
|
|
125
|
+
*/
|
|
126
|
+
export const RISK_LABELS = {
|
|
127
|
+
HIGH: 'risk::high',
|
|
128
|
+
};
|
|
129
|
+
|
|
114
130
|
/**
|
|
115
131
|
* Planning-axis labels (Epic #2880 F7). Currently scoped to the
|
|
116
132
|
* `planning::healthcheck-waived` operator-applied waiver — a historical
|
|
@@ -147,6 +163,7 @@ export const PLANNING_HEALTHCHECK_WAIVED = 'planning::healthcheck-waived';
|
|
|
147
163
|
*/
|
|
148
164
|
export const LABEL_COLORS = {
|
|
149
165
|
TYPE: '#7057FF',
|
|
166
|
+
RISK_HIGH: '#B60205',
|
|
150
167
|
AGENT: '#0E8A16',
|
|
151
168
|
STATUS_BLOCKED: '#D93F0B',
|
|
152
169
|
ACCEPTANCE: '#FBCA04',
|
|
@@ -534,18 +534,33 @@ function frictionForTerminal(envelope) {
|
|
|
534
534
|
* Emit the friction record (if any) implied by a terminal envelope.
|
|
535
535
|
* Best-effort; never throws.
|
|
536
536
|
*
|
|
537
|
+
* `tool` is the CALLER's name, threaded in rather than assumed: TWO CLIs emit
|
|
538
|
+
* terminal envelopes — `single-story-close.js` (plus its runner) and
|
|
539
|
+
* `single-story-confirm-merge.js` — and `retro-proposals.js` reads
|
|
540
|
+
* `emitter.tool` (via {@link normalizeGatheredSignal}) to name the surface a
|
|
541
|
+
* candidate came from, so hard-coding the close name here attributed every
|
|
542
|
+
* confirm-merge record to a CLI that never ran and misdirected the follow-up.
|
|
543
|
+
* Both callers already declare their name once via `runAsCli({ source })`;
|
|
544
|
+
* pass that same string. The default keeps the close paths — the original
|
|
545
|
+
* callers, which pass none — emitting exactly what they always did.
|
|
546
|
+
*
|
|
537
547
|
* @param {object} args
|
|
538
548
|
* @param {object} args.envelope
|
|
549
|
+
* @param {string} [args.tool] Emitting surface (default `single-story-close`).
|
|
539
550
|
* @param {object} [args.config]
|
|
540
551
|
* @returns {Promise<boolean>} true when a record was appended.
|
|
541
552
|
*/
|
|
542
|
-
export async function emitTerminalFriction({
|
|
553
|
+
export async function emitTerminalFriction({
|
|
554
|
+
envelope,
|
|
555
|
+
tool = 'single-story-close',
|
|
556
|
+
config,
|
|
557
|
+
} = {}) {
|
|
543
558
|
const verdict = frictionForTerminal(envelope);
|
|
544
559
|
if (!verdict) return false;
|
|
545
560
|
return emitRuntimeFriction({
|
|
546
561
|
storyId: envelope?.storyId,
|
|
547
562
|
category: verdict.category,
|
|
548
|
-
tool
|
|
563
|
+
tool,
|
|
549
564
|
details: verdict.details,
|
|
550
565
|
config,
|
|
551
566
|
});
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
* - `.agentrc.json`
|
|
18
18
|
* - `.claude/`
|
|
19
19
|
* - `node .agents/scripts/`
|
|
20
|
+
* …or (Story #4916) names one of the CLI entry points shipped at the
|
|
21
|
+
* top level of `.agents/scripts/` by **bare basename**, so recognition
|
|
22
|
+
* does not depend on how the caller spelled the reference.
|
|
20
23
|
* - Anything else (or empty input) defaults to `"consumer"`. The default
|
|
21
24
|
* is intentional — most friction comes from consumer code touching
|
|
22
25
|
* framework tooling, and we'd rather under-tag than mis-route a
|
|
@@ -47,6 +50,133 @@ const FRAMEWORK_PREFIXES = Object.freeze([
|
|
|
47
50
|
'node .agents/scripts/',
|
|
48
51
|
]);
|
|
49
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Basenames of the CLI entry points shipped at the top level of
|
|
55
|
+
* `.agents/scripts/` (Story #4916).
|
|
56
|
+
*
|
|
57
|
+
* The prefix scan above recognises a framework script only when the caller
|
|
58
|
+
* spelled a path — `node .agents/scripts/acceptance-eval.js` classifies
|
|
59
|
+
* `framework`, while the same script named bare (`acceptance-eval.js --story
|
|
60
|
+
* 4901`) matched no prefix and fell through to `consumer`, so a real framework
|
|
61
|
+
* defect was routed as consumer-actionable and discarded. Recognition must not
|
|
62
|
+
* depend on how the caller spelled the reference.
|
|
63
|
+
*
|
|
64
|
+
* Kept as **static data**, not a directory read: `classifySignalSource` is
|
|
65
|
+
* called once per signal and MUST stay pure — no filesystem I/O at classify
|
|
66
|
+
* time. The sync test in
|
|
67
|
+
* `tests/lib/observability/source-classifier.test.js` reads the real directory
|
|
68
|
+
* and fails when a script is added, renamed, or removed without this list
|
|
69
|
+
* being updated, so the set cannot go stale and silently restore the blind
|
|
70
|
+
* spot.
|
|
71
|
+
*
|
|
72
|
+
* Scoped to the top level deliberately: those are the entry points a command
|
|
73
|
+
* line names. Library modules under `.agents/scripts/lib/` carry generic
|
|
74
|
+
* basenames (`index.js`, `config.js`) that a consumer command could plausibly
|
|
75
|
+
* name, and widening to them would trade one mis-route for another.
|
|
76
|
+
*
|
|
77
|
+
* @type {readonly string[]}
|
|
78
|
+
*/
|
|
79
|
+
const FRAMEWORK_SCRIPT_BASENAMES = Object.freeze([
|
|
80
|
+
'acceptance-eval.js',
|
|
81
|
+
'agents-bootstrap-github.js',
|
|
82
|
+
'apply-quality-bootstrap.js',
|
|
83
|
+
'audit-baselines.js',
|
|
84
|
+
'audit-labels-bootstrap.js',
|
|
85
|
+
'audit-to-stories.js',
|
|
86
|
+
'boot-sweep.js',
|
|
87
|
+
'bootstrap.js',
|
|
88
|
+
'check-action-pinning.js',
|
|
89
|
+
'check-arch-cycles.js',
|
|
90
|
+
'check-baseline-drift.js',
|
|
91
|
+
'check-baselines.js',
|
|
92
|
+
'check-context-budget.js',
|
|
93
|
+
'check-cyclomatic.js',
|
|
94
|
+
'check-dead-exports.js',
|
|
95
|
+
'check-doc-links.js',
|
|
96
|
+
'check-gherkin-placeholders.js',
|
|
97
|
+
'check-lifecycle-doc-drift.js',
|
|
98
|
+
'check-lifecycle-lint.js',
|
|
99
|
+
'check-schema-references.js',
|
|
100
|
+
'check-test-temp-hygiene.js',
|
|
101
|
+
'check-windows-git-perf.js',
|
|
102
|
+
'check-workflow-citations.js',
|
|
103
|
+
'check-workflow-cli-lint.js',
|
|
104
|
+
'check-workflow-timeouts.js',
|
|
105
|
+
'cleanup-repo-test-temp.js',
|
|
106
|
+
'coverage-capture.js',
|
|
107
|
+
'deliver-light.js',
|
|
108
|
+
'deliver-recover.js',
|
|
109
|
+
'diagnose-friction.js',
|
|
110
|
+
'diagnose.js',
|
|
111
|
+
'drain-pending-cleanup.js',
|
|
112
|
+
'evidence-gate.js',
|
|
113
|
+
'generate-config-docs.js',
|
|
114
|
+
'generate-lens-checklists.js',
|
|
115
|
+
'generate-lifecycle-docs.js',
|
|
116
|
+
'generate-skills-index.js',
|
|
117
|
+
'generate-workflows-doc.js',
|
|
118
|
+
'git-cleanup.js',
|
|
119
|
+
'install-matrix-assert.js',
|
|
120
|
+
'lint-baseline.js',
|
|
121
|
+
'lint-issue-body.js',
|
|
122
|
+
'lint-label-vocabulary.js',
|
|
123
|
+
'mandrel-update-preflight.js',
|
|
124
|
+
'nav-registry-diff.js',
|
|
125
|
+
'notify.js',
|
|
126
|
+
'plan-context.js',
|
|
127
|
+
'plan-critics.js',
|
|
128
|
+
'plan-persist.js',
|
|
129
|
+
'plan-run-epilogue.js',
|
|
130
|
+
'post-structured-comment.js',
|
|
131
|
+
'pr-watch-with-update.js',
|
|
132
|
+
'provision-git-hooks.js',
|
|
133
|
+
'quality-preview.js',
|
|
134
|
+
'quality-watch.js',
|
|
135
|
+
'resolve-doc-tiers.js',
|
|
136
|
+
'resolve-stories.js',
|
|
137
|
+
'resync-status-column.js',
|
|
138
|
+
'run-coverage.js',
|
|
139
|
+
'run-lint.js',
|
|
140
|
+
'run-test-profile.js',
|
|
141
|
+
'run-tests.js',
|
|
142
|
+
'run-verify.js',
|
|
143
|
+
'signals-view.js',
|
|
144
|
+
'single-story-close.js',
|
|
145
|
+
'single-story-confirm-merge.js',
|
|
146
|
+
'single-story-init.js',
|
|
147
|
+
'stories-wave-tick.js',
|
|
148
|
+
'story-plan.js',
|
|
149
|
+
'sync-agentrc.js',
|
|
150
|
+
'sync-branch-from-base.js',
|
|
151
|
+
'sync-claude-agents.js',
|
|
152
|
+
'sync-claude-commands.js',
|
|
153
|
+
'test-isolate.js',
|
|
154
|
+
'test-wrapper.js',
|
|
155
|
+
'update-coverage-baseline.js',
|
|
156
|
+
'update-crap-baseline.js',
|
|
157
|
+
'update-duplication-baseline.js',
|
|
158
|
+
'update-maintainability-baseline.js',
|
|
159
|
+
'update-ticket-state.js',
|
|
160
|
+
'validate-docs-freshness.js',
|
|
161
|
+
'validate-skills.js',
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Membership index over {@link FRAMEWORK_SCRIPT_BASENAMES}, built once at
|
|
166
|
+
* module load so the per-signal scan is a hash lookup.
|
|
167
|
+
*
|
|
168
|
+
* @type {ReadonlySet<string>}
|
|
169
|
+
*/
|
|
170
|
+
const FRAMEWORK_SCRIPT_BASENAME_SET = new Set(FRAMEWORK_SCRIPT_BASENAMES);
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Characters a shell-ish command line can wrap a token in. Stripped from both
|
|
174
|
+
* ends before the membership test so `"acceptance-eval.js",` still resolves.
|
|
175
|
+
*
|
|
176
|
+
* @type {RegExp}
|
|
177
|
+
*/
|
|
178
|
+
const TOKEN_TRIM = /^[\s'"`(,;:]+|[\s'"`),;:]+$/g;
|
|
179
|
+
|
|
50
180
|
/**
|
|
51
181
|
* Normalise a value to a string for prefix scanning. Anything that is not
|
|
52
182
|
* a string (undefined, null, numbers, objects) becomes the empty string,
|
|
@@ -78,6 +208,44 @@ function containsFrameworkPrefix(haystack) {
|
|
|
78
208
|
return false;
|
|
79
209
|
}
|
|
80
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Return true when `haystack` names a top-level framework script by **bare
|
|
213
|
+
* basename** — no path segment at all (Story #4916).
|
|
214
|
+
*
|
|
215
|
+
* Whitespace-tokenised and matched whole: only a token that *equals* a known
|
|
216
|
+
* basename counts. A token carrying any path segment is deliberately left to
|
|
217
|
+
* {@link containsFrameworkPrefix} — `./tools/notify.js` is the consumer's own
|
|
218
|
+
* script and must stay `consumer`, while `.agents/scripts/notify.js` already
|
|
219
|
+
* matches a prefix. Likewise `my-notify.js` is not `notify.js`.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} haystack
|
|
222
|
+
* @returns {boolean}
|
|
223
|
+
*/
|
|
224
|
+
function containsFrameworkScriptBasename(haystack) {
|
|
225
|
+
if (haystack.length === 0) return false;
|
|
226
|
+
for (const rawToken of haystack.split(/\s+/)) {
|
|
227
|
+
const token = rawToken.replace(TOKEN_TRIM, '');
|
|
228
|
+
if (FRAMEWORK_SCRIPT_BASENAME_SET.has(token)) return true;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* True when `haystack` names the framework's own surface, by either
|
|
235
|
+
* recognition route: a path prefix, or a bare framework-script basename.
|
|
236
|
+
*
|
|
237
|
+
* Purely additive over {@link containsFrameworkPrefix} — every string that
|
|
238
|
+
* matched a prefix still matches here, so widening can only ever move a
|
|
239
|
+
* classification from `consumer` to `framework`, never the reverse.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} haystack
|
|
242
|
+
* @returns {boolean}
|
|
243
|
+
*/
|
|
244
|
+
function namesFrameworkSurface(haystack) {
|
|
245
|
+
if (containsFrameworkPrefix(haystack)) return true;
|
|
246
|
+
return containsFrameworkScriptBasename(haystack);
|
|
247
|
+
}
|
|
248
|
+
|
|
81
249
|
/**
|
|
82
250
|
* Classify a friction signal as `"framework"` or `"consumer"`.
|
|
83
251
|
*
|
|
@@ -92,6 +260,10 @@ function containsFrameworkPrefix(haystack) {
|
|
|
92
260
|
* `node .agents/scripts/single-story-init.js` — that's framework friction even
|
|
93
261
|
* though the failing test path lives under the consumer.
|
|
94
262
|
*
|
|
263
|
+
* Either input matches on a framework path prefix **or** on a bare framework
|
|
264
|
+
* script basename (Story #4916), so `single-story-close.js --story 4906` is
|
|
265
|
+
* recognised exactly like its fully-pathed spelling.
|
|
266
|
+
*
|
|
95
267
|
* @param {unknown} failingPath The path of the file or directory the
|
|
96
268
|
* signal blames (e.g. `"tests/foo.test.js"`).
|
|
97
269
|
* @param {unknown} command The command line the signal blames
|
|
@@ -101,8 +273,8 @@ function containsFrameworkPrefix(haystack) {
|
|
|
101
273
|
export function classifyPathSource(failingPath, command) {
|
|
102
274
|
const path = toScanString(failingPath);
|
|
103
275
|
const cmd = toScanString(command);
|
|
104
|
-
if (
|
|
105
|
-
if (
|
|
276
|
+
if (namesFrameworkSurface(path)) return 'framework';
|
|
277
|
+
if (namesFrameworkSurface(cmd)) return 'framework';
|
|
106
278
|
return 'consumer';
|
|
107
279
|
}
|
|
108
280
|
|
|
@@ -229,5 +401,6 @@ export function classifySignalSource(record) {
|
|
|
229
401
|
|
|
230
402
|
export const __testing = Object.freeze({
|
|
231
403
|
FRAMEWORK_PREFIXES,
|
|
404
|
+
FRAMEWORK_SCRIPT_BASENAMES,
|
|
232
405
|
TOOL_DEGRADED_CATEGORY,
|
|
233
406
|
});
|
|
@@ -17,6 +17,20 @@
|
|
|
17
17
|
* from the cap enforcement path — a critic that always reports `round: 1`
|
|
18
18
|
* can no longer defeat the bounded-loop guarantee.
|
|
19
19
|
*
|
|
20
|
+
* ## Reading a verdict is not a round (Story #4874)
|
|
21
|
+
*
|
|
22
|
+
* Counting signals made *observation* costly: re-running the gate over an
|
|
23
|
+
* already-scored verdict — a resumed sub-agent re-reading its own verdict,
|
|
24
|
+
* an operator re-checking why the loop said `redraft` — appended another
|
|
25
|
+
* signal and advanced the derived round, so a `redraft` could escalate to
|
|
26
|
+
* `block` with no work in between. Every appended signal therefore carries
|
|
27
|
+
* a `details.verdictFingerprint` content-addressing the verdict it scored,
|
|
28
|
+
* and {@link resolveAcceptanceEvalRound} replays the round already recorded
|
|
29
|
+
* for that fingerprint instead of minting a new one. A replay is observably
|
|
30
|
+
* free: the round does not advance and no signal is appended. New work
|
|
31
|
+
* produces different verdict content, hence a new fingerprint, hence a
|
|
32
|
+
* genuine round.
|
|
33
|
+
*
|
|
20
34
|
* ## The three terminal actions
|
|
21
35
|
*
|
|
22
36
|
* - `proceed` — every criterion is `met`. The Story may flip to
|
|
@@ -39,6 +53,7 @@
|
|
|
39
53
|
* possible action is `block`.
|
|
40
54
|
*/
|
|
41
55
|
|
|
56
|
+
import { createHash } from 'node:crypto';
|
|
42
57
|
import { readFileSync } from 'node:fs';
|
|
43
58
|
|
|
44
59
|
import { runArtifactPath, signalsFile } from '../config/temp-paths.js';
|
|
@@ -112,7 +127,7 @@ function partitionCriteria(criteria) {
|
|
|
112
127
|
* The resolved (already-clamped) redraft ceiling from
|
|
113
128
|
* `getAcceptanceEval(config).maxRounds`.
|
|
114
129
|
* @param {number} [args.round]
|
|
115
|
-
* The current round number, derived via `
|
|
130
|
+
* The current round number, derived via `resolveAcceptanceEvalRound`.
|
|
116
131
|
* Defaults to 1 when absent or invalid.
|
|
117
132
|
* @returns {{
|
|
118
133
|
* decision: 'proceed' | 'redraft' | 'block',
|
|
@@ -172,6 +187,7 @@ export function buildAcceptanceEvalSignal({
|
|
|
172
187
|
outcome,
|
|
173
188
|
phase = 'implement',
|
|
174
189
|
clusterId = null,
|
|
190
|
+
verdictFingerprint = null,
|
|
175
191
|
}) {
|
|
176
192
|
return {
|
|
177
193
|
kind: 'acceptance-eval',
|
|
@@ -186,6 +202,13 @@ export function buildAcceptanceEvalSignal({
|
|
|
186
202
|
phase,
|
|
187
203
|
emitter: { tool: 'acceptance-eval.js' },
|
|
188
204
|
details: {
|
|
205
|
+
// Content address of the verdict this signal scored. The replay guard
|
|
206
|
+
// in `resolveAcceptanceEvalRound` matches on it, so a re-read of an
|
|
207
|
+
// already-scored verdict reuses its round instead of minting one.
|
|
208
|
+
...(typeof verdictFingerprint === 'string' &&
|
|
209
|
+
verdictFingerprint.length > 0
|
|
210
|
+
? { verdictFingerprint }
|
|
211
|
+
: {}),
|
|
189
212
|
decision: outcome.decision,
|
|
190
213
|
round: outcome.round,
|
|
191
214
|
cap: outcome.cap,
|
|
@@ -201,29 +224,29 @@ export function buildAcceptanceEvalSignal({
|
|
|
201
224
|
}
|
|
202
225
|
|
|
203
226
|
/**
|
|
204
|
-
*
|
|
205
|
-
* `
|
|
206
|
-
*
|
|
207
|
-
* first run reports round 1 and each completed round (which appends one
|
|
208
|
-
* signal via `acceptance-eval.js`) advances the derived round by one.
|
|
227
|
+
* Read the `acceptance-eval` records already appended to the Story's (or AC
|
|
228
|
+
* cluster's) `signals.ndjson`, in append order — the prior rounds
|
|
229
|
+
* {@link resolveAcceptanceEvalRound} counts from (Story #4019).
|
|
209
230
|
*
|
|
210
|
-
* The
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* gate.
|
|
231
|
+
* The read is restart-safe: the ledger lives on disk, so a subagent that
|
|
232
|
+
* dies mid-loop and restarts still observes every prior round. A missing or
|
|
233
|
+
* unreadable ledger degrades to "no prior rounds" and malformed lines are
|
|
234
|
+
* skipped — observability corruption never wedges the gate.
|
|
215
235
|
*
|
|
216
236
|
* @param {object} args
|
|
217
237
|
* @param {number|null} args.epicId Parent Epic ID, or `null` for a
|
|
218
238
|
* standalone Story (routes to `<tempRoot>/standalone/stories/...`).
|
|
219
239
|
* @param {number} args.storyId
|
|
240
|
+
* @param {string|null} [args.clusterId] AC-cluster id (Epic #4475 M4-B).
|
|
220
241
|
* @param {object} [args.config] Resolved config (tempRoot resolution).
|
|
221
242
|
* @param {(p: string) => string} [args.readFile] Injectable reader (tests).
|
|
222
243
|
* @param {(eid: number|null, sid: number, config?: object) => string} [args.signalsPathResolver]
|
|
223
244
|
* Injectable path resolver (tests). Defaults to `signalsFile`.
|
|
224
|
-
* @
|
|
245
|
+
* @param {(eid: number, config?: object) => string} [args.epicSignalsPathResolver]
|
|
246
|
+
* Injectable epic-stream path resolver (tests).
|
|
247
|
+
* @returns {object[]} The matching records, oldest first.
|
|
225
248
|
*/
|
|
226
|
-
|
|
249
|
+
function readPriorAcceptanceEvalRecords({
|
|
227
250
|
epicId,
|
|
228
251
|
storyId,
|
|
229
252
|
clusterId = null,
|
|
@@ -249,10 +272,10 @@ export function deriveAcceptanceEvalRound({
|
|
|
249
272
|
: readFile(signalsPathResolver(epicId ?? null, storyId, config));
|
|
250
273
|
} catch (_err) {
|
|
251
274
|
// No ledger yet → no prior rounds.
|
|
252
|
-
return
|
|
275
|
+
return [];
|
|
253
276
|
}
|
|
254
277
|
|
|
255
|
-
|
|
278
|
+
const records = [];
|
|
256
279
|
for (const line of String(text).split('\n')) {
|
|
257
280
|
const trimmed = line.trim();
|
|
258
281
|
if (trimmed === '') continue;
|
|
@@ -269,7 +292,77 @@ export function deriveAcceptanceEvalRound({
|
|
|
269
292
|
} else if (record.storyId !== storyId) {
|
|
270
293
|
continue;
|
|
271
294
|
}
|
|
272
|
-
|
|
295
|
+
records.push(record);
|
|
273
296
|
}
|
|
274
|
-
return
|
|
297
|
+
return records;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Content-address a verdict so re-scoring the same verdict is recognisable
|
|
302
|
+
* as a re-read rather than a new round (Story #4874).
|
|
303
|
+
*
|
|
304
|
+
* The fingerprint covers exactly what the decision depends on — the ordered
|
|
305
|
+
* per-criterion `index` / `criterion` / `verdict` / `evidence` tuples — and
|
|
306
|
+
* deliberately nothing else: the verdict's self-reported `round`, its
|
|
307
|
+
* timestamp, and any authoring scratch must not make an unchanged
|
|
308
|
+
* evaluation look like a new one. Conversely, real rework changes at least
|
|
309
|
+
* one criterion's verdict or its evidence, so a genuine re-evaluation
|
|
310
|
+
* always fingerprints differently.
|
|
311
|
+
*
|
|
312
|
+
* @param {{ criteria?: Array<object> }} verdict
|
|
313
|
+
* @returns {string} 16 hex chars of a SHA-256 over the canonical form.
|
|
314
|
+
*/
|
|
315
|
+
export function computeVerdictFingerprint(verdict) {
|
|
316
|
+
const criteria = Array.isArray(verdict?.criteria) ? verdict.criteria : [];
|
|
317
|
+
const canonical = criteria.map((c) => [
|
|
318
|
+
Number.isInteger(c?.index) ? c.index : null,
|
|
319
|
+
typeof c?.criterion === 'string' ? c.criterion : '',
|
|
320
|
+
typeof c?.verdict === 'string' ? c.verdict : '',
|
|
321
|
+
typeof c?.evidence === 'string' ? c.evidence : '',
|
|
322
|
+
]);
|
|
323
|
+
return createHash('sha256')
|
|
324
|
+
.update(JSON.stringify(canonical))
|
|
325
|
+
.digest('hex')
|
|
326
|
+
.slice(0, 16);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Resolve the round a verdict should be scored under, distinguishing a
|
|
331
|
+
* genuine evaluation from a re-read of an already-scored verdict
|
|
332
|
+
* (Story #4874). A genuine round is `prior-signal count + 1`, so the first
|
|
333
|
+
* run reports round 1 and each completed round advances it by one.
|
|
334
|
+
*
|
|
335
|
+
* When the ledger already carries an `acceptance-eval` signal whose
|
|
336
|
+
* `details.verdictFingerprint` matches, this invocation is a **replay**: it
|
|
337
|
+
* reports that signal's round and tells the caller not to append a second
|
|
338
|
+
* one, so reading is observably free and cannot escalate a `redraft` into a
|
|
339
|
+
* `block`. Otherwise it is a genuine round and the counter advances exactly
|
|
340
|
+
* as the pre-#4874 count-based derivation always did.
|
|
341
|
+
*
|
|
342
|
+
* Signals written before this field existed carry no fingerprint; they can
|
|
343
|
+
* never match, so legacy ledgers keep their count-based behaviour.
|
|
344
|
+
*
|
|
345
|
+
* @param {object} args — {@link readPriorAcceptanceEvalRecords}'s arguments
|
|
346
|
+
* plus:
|
|
347
|
+
* @param {string} args.verdictFingerprint
|
|
348
|
+
* @returns {{ round: number, replay: boolean }}
|
|
349
|
+
*/
|
|
350
|
+
export function resolveAcceptanceEvalRound(args) {
|
|
351
|
+
const { verdictFingerprint } = args;
|
|
352
|
+
const records = readPriorAcceptanceEvalRecords(args);
|
|
353
|
+
const priorIndex =
|
|
354
|
+
typeof verdictFingerprint === 'string' && verdictFingerprint.length > 0
|
|
355
|
+
? records.findIndex(
|
|
356
|
+
(r) => r?.details?.verdictFingerprint === verdictFingerprint,
|
|
357
|
+
)
|
|
358
|
+
: -1;
|
|
359
|
+
if (priorIndex === -1) {
|
|
360
|
+
return { round: records.length + 1, replay: false };
|
|
361
|
+
}
|
|
362
|
+
const recorded = records[priorIndex]?.details?.round;
|
|
363
|
+
return {
|
|
364
|
+
round:
|
|
365
|
+
Number.isInteger(recorded) && recorded >= 1 ? recorded : priorIndex + 1,
|
|
366
|
+
replay: true,
|
|
367
|
+
};
|
|
275
368
|
}
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
* @typedef {'fresh'|'inline'} CeremonyMode
|
|
79
79
|
* @typedef {'fresh-critic'|'inline-self-eval'} VerdictOwner
|
|
80
80
|
* @typedef {import('./review-depth.js').ChangeLevel} ChangeLevel
|
|
81
|
-
* @typedef {
|
|
81
|
+
* @typedef {(typeof CEREMONY_PROFILES)[number]} CeremonyProfile
|
|
82
82
|
*/
|
|
83
83
|
|
|
84
84
|
/**
|
|
@@ -93,12 +93,18 @@ export function verdictOwnerForMode(mode) {
|
|
|
93
93
|
return mode === 'fresh' ? 'fresh-critic' : 'inline-self-eval';
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
/**
|
|
97
|
+
* The ceremony-profile vocabulary — the **single** place the three profile
|
|
98
|
+
* names are written. `normalizeCeremonyProfile` is its reader and the
|
|
99
|
+
* `CeremonyProfile` typedef is derived from it, so adding a profile is a
|
|
100
|
+
* one-line change here (Story #4926).
|
|
101
|
+
*
|
|
102
|
+
* @type {readonly ['minimal', 'standard', 'strict']}
|
|
103
|
+
*/
|
|
104
|
+
const CEREMONY_PROFILES = Object.freeze(['minimal', 'standard', 'strict']);
|
|
105
|
+
|
|
106
|
+
/** The profile an absent or unrecognized value degrades to. */
|
|
107
|
+
const DEFAULT_CEREMONY_PROFILE = 'standard';
|
|
102
108
|
|
|
103
109
|
/**
|
|
104
110
|
* Normalize an operator/config ceremony profile. Unknown values degrade to
|
|
@@ -107,11 +113,10 @@ export const CEREMONY_PROFILES = Object.freeze([
|
|
|
107
113
|
* @param {unknown} value
|
|
108
114
|
* @returns {CeremonyProfile}
|
|
109
115
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return 'standard';
|
|
116
|
+
function normalizeCeremonyProfile(value) {
|
|
117
|
+
return CEREMONY_PROFILES.includes(/** @type {CeremonyProfile} */ (value))
|
|
118
|
+
? /** @type {CeremonyProfile} */ (value)
|
|
119
|
+
: DEFAULT_CEREMONY_PROFILE;
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
/**
|