@event4u/agent-config 14.13.0 → 14.14.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +116 -387
- package/dist/agent-src/commands/analyze/inbox.md +36 -0
- package/dist/agent-src/rules/code-comment-discipline.md +6 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/dist/agent-src/scripts/guarded_baseline.ts +22 -7
- package/dist/agent-src/scripts/stubs_due.ts +118 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/cmd_conformance.js +38 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +4 -4
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1438 -711
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +112 -17
- package/docs/guidelines/agent-infra/guarded-baseline.md +1 -1
- package/docs/guidelines/code-clarity.md +18 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/src/agent-src/scripts/guarded_baseline.ts +22 -7
- package/src/agent-src/scripts/stubs_due.ts +118 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/src/config/estate-count-budget.json +20 -1
- package/src/config/gate-coverage.yml +150 -0
- package/src/config/gate-violation-baselines.json +8 -3
- package/src/config/preamble-payload-budget.json +19 -11
- package/src/config/rule-stub-ceilings.json +14 -7
- package/src/scripts/_dispatch.bash +0 -4
- package/src/scripts/_lib/activation_ladder.ts +47 -3
- package/src/scripts/_lib/activation_receipt_producer.ts +341 -0
- package/src/scripts/_lib/adr_frontmatter.ts +26 -0
- package/src/scripts/_lib/candidate_pair_delta.ts +349 -0
- package/src/scripts/_lib/carrier_status.ts +118 -0
- package/src/scripts/_lib/cascade_stage_enumeration.ts +92 -0
- package/src/scripts/_lib/corpus_manifest.ts +726 -0
- package/src/scripts/_lib/delivery_arm_experiment.ts +18 -2
- package/src/scripts/_lib/estate_carrier_cases.ts +72 -0
- package/src/scripts/_lib/evaluation_cascade.ts +452 -0
- package/src/scripts/_lib/evolution_roi.ts +562 -0
- package/src/scripts/_lib/harness_evolution_guards.ts +10 -0
- package/src/scripts/_lib/lexical_shortlist.ts +215 -0
- package/src/scripts/_lib/llm_candidate_proposer.ts +461 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +153 -0
- package/src/scripts/_lib/pathology_archive.ts +393 -0
- package/src/scripts/_lib/promotion_capability.ts +315 -0
- package/src/scripts/_lib/promotion_evidence.ts +414 -0
- package/src/scripts/_lib/promotion_review.ts +294 -0
- package/src/scripts/_lib/prompt_shape.ts +92 -0
- package/src/scripts/_lib/release_highlights.ts +70 -1
- package/src/scripts/_lib/release_material.ts +125 -2
- package/src/scripts/_lib/routing_corpus.ts +307 -0
- package/src/scripts/_lib/routing_index_input.ts +104 -0
- package/src/scripts/_lib/rule_injection.ts +22 -1
- package/src/scripts/_lib/semantic_noop.ts +167 -0
- package/src/scripts/activation_receipt.ts +258 -0
- package/src/scripts/ai_council/cross_exam.ts +128 -0
- package/src/scripts/ai_council/explain_route.ts +157 -0
- package/src/scripts/ai_council/information_gain.ts +293 -0
- package/src/scripts/ai_council/leakage_corpus.ts +385 -0
- package/src/scripts/ai_council/leakage_patterns.ts +453 -0
- package/src/scripts/ai_council/minority_retention.ts +171 -0
- package/src/scripts/ai_council/recouncil_savings.ts +299 -0
- package/src/scripts/ai_council/replay_route.ts +223 -0
- package/src/scripts/ai_council/reviewer_assignment.ts +279 -0
- package/src/scripts/ai_council/routing_training_row.ts +197 -0
- package/src/scripts/ai_council/synthesis_strategy.ts +159 -0
- package/src/scripts/check_adr_frontmatter.ts +4 -9
- package/src/scripts/check_estate_count.ts +205 -24
- package/src/scripts/check_release_highlights.ts +96 -13
- package/src/scripts/check_roadmap_trackable.ts +17 -2
- package/src/scripts/corpus_manifest.ts +235 -0
- package/src/scripts/evolution_lab.ts +181 -8
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +28 -6
- package/src/scripts/hooks/comment_discipline_hook.ts +146 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
- package/src/scripts/language_mirror_hook.ts +14 -2
- package/src/scripts/lint_carrier_integrity.ts +820 -0
- package/src/scripts/lint_code_comments.ts +643 -0
- package/src/scripts/lint_plan_risk_register.ts +39 -19
- package/src/scripts/lint_promotion_paths.ts +779 -0
- package/src/scripts/llm_propose.ts +256 -0
- package/src/scripts/measure_delivery_sets.ts +361 -0
- package/src/scripts/measure_routing_signal.ts +307 -0
- package/src/scripts/model_rule_injection.ts +19 -3
- package/src/scripts/probe_council_retention.ts +495 -0
- package/src/scripts/release.ts +21 -37
- package/src/scripts/release_drill.ts +143 -0
- package/src/scripts/release_env.ts +35 -0
- package/src/scripts/release_publication.ts +84 -0
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Standing validator for deferral carries whose parent is already archived.
|
|
4
|
+
*
|
|
5
|
+
* A `[~]` step carried to a follow-up roadmap is checked once, at the moment
|
|
6
|
+
* the parent is archived, and never again. The check that performs it runs
|
|
7
|
+
* inside a sweep over active roadmaps, so once the parent moves under
|
|
8
|
+
* `archive/` neither end of the pair is ever looked at together again. The
|
|
9
|
+
* receiver can then be deleted, renamed, or stripped of its back-link and
|
|
10
|
+
* nothing goes red.
|
|
11
|
+
*
|
|
12
|
+
* This gate closes that by walking from the ARCHIVED side. An archived roadmap
|
|
13
|
+
* is a durable record: nothing rewrites it, and it names its destination by
|
|
14
|
+
* slug. So the expected set of live receivers is derivable at any time without
|
|
15
|
+
* a registry to maintain, and whole-file deletion is detected on the first
|
|
16
|
+
* pass — which a validator enumerating only the surviving receivers cannot do.
|
|
17
|
+
*
|
|
18
|
+
* Annotation parsing is delegated to `parseDeferredItems`, the same function the
|
|
19
|
+
* archival sweep uses, so the syntax has one implementation and cannot drift.
|
|
20
|
+
* The RESOLUTION POLICY is deliberately not shared, and the reason is temporal
|
|
21
|
+
* rather than accidental. At archival time a destination under `archive/` is a
|
|
22
|
+
* refusal: carrying an item into an already-dead roadmap loses it on arrival.
|
|
23
|
+
* Afterwards, a destination that completed its own work and archived is the
|
|
24
|
+
* SUCCESS case, not a loss. Sharing that branch would red two carries in this
|
|
25
|
+
* repository whose receivers did exactly what they were asked to do — measured,
|
|
26
|
+
* not predicted: both were reported on the first run against real data.
|
|
27
|
+
*
|
|
28
|
+
* So the standing policy is: the destination must still exist somewhere (a
|
|
29
|
+
* missing file is the deletion this gate is for), it must still back-link, and
|
|
30
|
+
* if it is archived it must carry no OPEN steps. An archived roadmap with open
|
|
31
|
+
* steps was moved by hand and its obligations are stranded. A destination under
|
|
32
|
+
* `skipped/` is red in either policy — skipping is not fulfilment.
|
|
33
|
+
*
|
|
34
|
+
* Unsupported transitions fail closed rather than being inferred. A rename
|
|
35
|
+
* breaks destination resolution. A re-parent breaks the back-link. An onward
|
|
36
|
+
* carry that removes the intermediate breaks the original parent's link. All
|
|
37
|
+
* three are red here, deliberately, until a disposition vocabulary exists to
|
|
38
|
+
* express them.
|
|
39
|
+
*
|
|
40
|
+
* A SECOND ENUMERATION runs beside the first, over the live carriers rather
|
|
41
|
+
* than over the archived parents, and it exists because the walk direction that
|
|
42
|
+
* makes whole-file deletion detectable also makes a self-declared carrier
|
|
43
|
+
* invisible. `status: carrier` buys exclusion from the active count, from
|
|
44
|
+
* trackability and from the plan risk register; enumerating only archived
|
|
45
|
+
* parents means a file nobody carried anything to wears the status for free.
|
|
46
|
+
* So every live carrier must be named by some dead roadmap's `carried-to=`
|
|
47
|
+
* annotation, and one that is not is a hard failure like any other lost
|
|
48
|
+
* obligation — the status is legitimate for a receiver of a real carry, and for
|
|
49
|
+
* nothing else.
|
|
50
|
+
*
|
|
51
|
+
* Exit codes: 0 = every carry resolves, 1 = at least one does not.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import * as fs from 'node:fs';
|
|
55
|
+
import * as os from 'node:os';
|
|
56
|
+
import * as path from 'node:path';
|
|
57
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
58
|
+
|
|
59
|
+
import {
|
|
60
|
+
parseDeferredItems,
|
|
61
|
+
_relatesTo as relatesTo,
|
|
62
|
+
type DeferredItem,
|
|
63
|
+
} from '../agent-src/scripts/archive_completed_roadmaps.js';
|
|
64
|
+
import { checkRatchet } from './_lib/gate_baseline.js';
|
|
65
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
66
|
+
import { reportScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
67
|
+
import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
|
|
68
|
+
|
|
69
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
70
|
+
const _DEFAULT_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
71
|
+
|
|
72
|
+
/** Directories holding roadmaps that can no longer be re-checked by the sweep. */
|
|
73
|
+
const DEAD_DIRS: readonly string[] = ['archive', 'skipped'];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Cheap pre-filter. It must match a BARE `[~]` too, not only the annotation:
|
|
77
|
+
* a deferred step with no annotation at all is the loss case, and filtering on
|
|
78
|
+
* the annotation alone made the gate blind to exactly the shape it exists for.
|
|
79
|
+
* Its own self-test caught that, which is the difference between a `scanned:`
|
|
80
|
+
* floor and a proof that the reading changes the verdict.
|
|
81
|
+
*/
|
|
82
|
+
const CARRY_CANDIDATE_RE = /deferred-resolution:|^[ \t]*[-*][ \t]*\[~\]/m;
|
|
83
|
+
|
|
84
|
+
const EXCLUDE_NAMES: ReadonlySet<string> = new Set([
|
|
85
|
+
'README.md',
|
|
86
|
+
'INDEX.md',
|
|
87
|
+
'template.md',
|
|
88
|
+
'progress.md',
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Two classes, and the split is the whole design.
|
|
93
|
+
*
|
|
94
|
+
* `unannotated` — a `[~]` step with no `deferred-resolution:` comment at all.
|
|
95
|
+
* Every one of the 243 in this repository predates the annotation contract,
|
|
96
|
+
* which was settled by council three months after the newest of them. It is
|
|
97
|
+
* real debt, it is shrink-only, and it is not what this gate was built for.
|
|
98
|
+
*
|
|
99
|
+
* `broken-destination` — an annotation that named a receiver which no longer
|
|
100
|
+
* holds up its end: deleted, renamed, re-parented, skipped, or archived with
|
|
101
|
+
* open steps. This class is a hard failure at zero, because every member of it
|
|
102
|
+
* is an obligation that had a recorded home and lost it. Deleting a live
|
|
103
|
+
* receiver produces this class, not the other one.
|
|
104
|
+
*/
|
|
105
|
+
export type CarrierProblemClass = 'unannotated' | 'broken-destination';
|
|
106
|
+
|
|
107
|
+
export interface CarrierProblem {
|
|
108
|
+
/** Repo-relative path of the archived roadmap whose carry is broken. */
|
|
109
|
+
source: string;
|
|
110
|
+
cls: CarrierProblemClass;
|
|
111
|
+
detail: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Roadmap files under the dead directories, sorted for stable output. */
|
|
115
|
+
export function deadRoadmaps(root: string): string[] {
|
|
116
|
+
const out: string[] = [];
|
|
117
|
+
for (const dir of DEAD_DIRS) {
|
|
118
|
+
const abs = path.join(root, 'agents', 'roadmaps', dir);
|
|
119
|
+
let entries: string[];
|
|
120
|
+
try {
|
|
121
|
+
entries = fs.readdirSync(abs);
|
|
122
|
+
} catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
for (const name of entries.sort()) {
|
|
126
|
+
if (!name.endsWith('.md') || EXCLUDE_NAMES.has(name)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const full = path.join(abs, name);
|
|
130
|
+
if (!fs.statSync(full).isFile()) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
out.push(path.posix.join('agents/roadmaps', dir, name));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Escape a slug for literal use inside a RegExp. */
|
|
140
|
+
function _escapeRe(v: string): string {
|
|
141
|
+
return v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The frontmatter block's body, or `''` when the file opens without one.
|
|
146
|
+
*
|
|
147
|
+
* Every structural test below reads this rather than the whole file. A roadmap
|
|
148
|
+
* body legitimately quotes roadmap syntax — that is what the stub recording the
|
|
149
|
+
* deferred transition vocabulary does — so a whole-file regex answers a question
|
|
150
|
+
* about documentation as if it were a question about declarations.
|
|
151
|
+
*/
|
|
152
|
+
export function frontmatter(text: string): string {
|
|
153
|
+
const m = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
154
|
+
return m === null ? '' : (m[1] ?? '');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* True when `text` DECLARES `parent_roadmap: <slug>` or a `relates:` row naming
|
|
159
|
+
* it, in its frontmatter.
|
|
160
|
+
*
|
|
161
|
+
* Frontmatter-scoped, and the scope is the check. The first version matched
|
|
162
|
+
* `- slug: <parent>` anywhere in the file, which re-opened the defect a prior
|
|
163
|
+
* review closed in `deferralProblems`: a mention in prose, in an example, or
|
|
164
|
+
* inside a fenced block is not a declaration, and a back-link that prose can
|
|
165
|
+
* satisfy proves nothing about either end of the pair. The `relates:` walk is
|
|
166
|
+
* imported from the archival sweep rather than re-implemented, for the same
|
|
167
|
+
* one-implementation reason the annotation parser is.
|
|
168
|
+
*/
|
|
169
|
+
function _linksBackTo(text: string, slug: string): boolean {
|
|
170
|
+
const fm = frontmatter(text);
|
|
171
|
+
if (fm === '') {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
if (new RegExp(`^parent_roadmap:[ \t]*${_escapeRe(slug)}[ \t]*$`, 'm').test(fm)) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
return relatesTo(fm, slug);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Where a destination slug resolves, and under which directory. */
|
|
181
|
+
function _locate(root: string, slug: string): { file: string; dir: 'active' | 'later' | 'archive' | 'skipped' } | null {
|
|
182
|
+
const candidates: ReadonlyArray<readonly [string, 'active' | 'later' | 'archive' | 'skipped']> = [
|
|
183
|
+
[path.join(root, 'agents', 'roadmaps', `${slug}.md`), 'active'],
|
|
184
|
+
[path.join(root, 'agents', 'roadmaps', 'later', `${slug}.md`), 'later'],
|
|
185
|
+
[path.join(root, 'agents', 'roadmaps', 'archive', `${slug}.md`), 'archive'],
|
|
186
|
+
[path.join(root, 'agents', 'roadmaps', 'skipped', `${slug}.md`), 'skipped'],
|
|
187
|
+
];
|
|
188
|
+
for (const [file, dir] of candidates) {
|
|
189
|
+
if (fs.existsSync(file)) {
|
|
190
|
+
return { file, dir };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** An OPEN checkbox on one line — `-` or `*`, matching the canonical grammar. */
|
|
197
|
+
const OPEN_STEP_RE = /^[ \t]*[-*][ \t]*\[[ ][ \t]*\][ \t]/;
|
|
198
|
+
/** A fenced block's delimiter, in either of the two fence styles in use here. */
|
|
199
|
+
const FENCE_RE = /^[ \t]*(?:```|~~~)/;
|
|
200
|
+
const CARRIER_STATUS_RE = /^status:[ \t]*carrier[ \t]*$/m;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* True when an OPEN step appears OUTSIDE every fenced block.
|
|
204
|
+
*
|
|
205
|
+
* Fence-aware because this branch hard-fails at zero with no baseline, and a
|
|
206
|
+
* roadmap that documents step syntax inside a fenced block is not a roadmap
|
|
207
|
+
* with an open step. Measured before the fix: a fenced `markdown` example
|
|
208
|
+
* containing one `- [ ]` line reddened CI with a diagnostic naming the wrong
|
|
209
|
+
* cause and offering remediation that did not apply.
|
|
210
|
+
*/
|
|
211
|
+
export function hasOpenStep(text: string): boolean {
|
|
212
|
+
let inFence = false;
|
|
213
|
+
for (const line of text.split('\n')) {
|
|
214
|
+
if (FENCE_RE.test(line)) {
|
|
215
|
+
inFence = !inFence;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!inFence && OPEN_STEP_RE.test(line)) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** True when the frontmatter — not the body — declares `status: carrier`. */
|
|
226
|
+
export function declaresCarrier(text: string): boolean {
|
|
227
|
+
return CARRIER_STATUS_RE.test(frontmatter(text));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The standing carry policy for one archived roadmap. See the module header for
|
|
232
|
+
* why this is not `deferralProblems`.
|
|
233
|
+
*/
|
|
234
|
+
export function carryProblems(
|
|
235
|
+
root: string,
|
|
236
|
+
sourceSlug: string,
|
|
237
|
+
text: string,
|
|
238
|
+
): Array<{ cls: CarrierProblemClass; detail: string }> {
|
|
239
|
+
return carryProblemsFor(root, sourceSlug, parseDeferredItems(text));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The same policy over an ALREADY-PARSED item list.
|
|
244
|
+
*
|
|
245
|
+
* Split out so the walk parses each candidate once and reuses the result for
|
|
246
|
+
* both this check and the destination census the live-carrier enumeration
|
|
247
|
+
* needs, rather than parsing the same file twice per run.
|
|
248
|
+
*/
|
|
249
|
+
export function carryProblemsFor(
|
|
250
|
+
root: string,
|
|
251
|
+
sourceSlug: string,
|
|
252
|
+
items: readonly DeferredItem[],
|
|
253
|
+
): Array<{ cls: CarrierProblemClass; detail: string }> {
|
|
254
|
+
const problems: Array<{ cls: CarrierProblemClass; detail: string }> = [];
|
|
255
|
+
for (const item of items) {
|
|
256
|
+
const label = item.text === '' ? '(unnamed step)' : item.text;
|
|
257
|
+
if (item.kind === null || item.destination === null) {
|
|
258
|
+
problems.push({
|
|
259
|
+
cls: 'unannotated',
|
|
260
|
+
detail:
|
|
261
|
+
`deferred step ${JSON.stringify(label)} carries no ` +
|
|
262
|
+
'`<!-- deferred-resolution: carried-to=<slug> -->` annotation, so nothing records where it went',
|
|
263
|
+
});
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (item.destination === sourceSlug) {
|
|
267
|
+
problems.push({
|
|
268
|
+
cls: 'broken-destination',
|
|
269
|
+
detail: `deferred step ${JSON.stringify(label)} names its OWN roadmap as the destination`,
|
|
270
|
+
});
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const found = _locate(root, item.destination);
|
|
274
|
+
if (found === null) {
|
|
275
|
+
problems.push({
|
|
276
|
+
cls: 'broken-destination',
|
|
277
|
+
detail: `deferred step ${JSON.stringify(label)} names destination \`${item.destination}\`, ` +
|
|
278
|
+
'which no longer exists anywhere under `agents/roadmaps/` — the receiver was deleted ' +
|
|
279
|
+
'and the obligation went with it',
|
|
280
|
+
});
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (found.dir === 'skipped') {
|
|
284
|
+
problems.push({
|
|
285
|
+
cls: 'broken-destination',
|
|
286
|
+
detail: `destination \`${item.destination}\` is under \`skipped/\` — skipping a receiver is not ` +
|
|
287
|
+
'fulfilling the carry',
|
|
288
|
+
});
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const destText = fs.readFileSync(found.file, 'utf-8');
|
|
292
|
+
// A carrier exists to stay live. Archiving one is the terminal-archival
|
|
293
|
+
// transition that has no vocabulary yet, and it strands every item the
|
|
294
|
+
// carrier itself holds — none of which is an OPEN step, so the check
|
|
295
|
+
// below would not see it.
|
|
296
|
+
if (found.dir === 'archive' && declaresCarrier(destText)) {
|
|
297
|
+
problems.push({
|
|
298
|
+
cls: 'broken-destination',
|
|
299
|
+
detail:
|
|
300
|
+
`destination \`${item.destination}\` is a \`status: carrier\` roadmap that has been ` +
|
|
301
|
+
'archived — a carrier holds obligations whose triggers are unmet, so archiving it ' +
|
|
302
|
+
'strands them. Terminal archival of a live carrier is not expressible yet and fails closed.',
|
|
303
|
+
});
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (found.dir === 'archive' && hasOpenStep(destText)) {
|
|
307
|
+
problems.push({
|
|
308
|
+
cls: 'broken-destination',
|
|
309
|
+
detail: `destination \`${item.destination}\` is archived but still has OPEN steps — it was moved ` +
|
|
310
|
+
'by hand rather than completed, so the carried obligation is stranded',
|
|
311
|
+
});
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (!_linksBackTo(destText, sourceSlug)) {
|
|
315
|
+
problems.push({
|
|
316
|
+
cls: 'broken-destination',
|
|
317
|
+
detail: `destination \`${item.destination}\` carries no \`parent_roadmap: ${sourceSlug}\` ` +
|
|
318
|
+
'back-link and no `relates:` row naming it — the link must verify from both ends',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return problems;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** The live locations a carrier may occupy — both are live per `_locate`. */
|
|
326
|
+
const LIVE_SUBDIRS: readonly string[] = ['', 'later'];
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Every roadmap that DECLARES `status: carrier` and is still live.
|
|
330
|
+
*
|
|
331
|
+
* The enumeration the walk-from-the-archive direction cannot produce. Sorted,
|
|
332
|
+
* repo-relative, and `later/` is included because `_locate` treats a parked
|
|
333
|
+
* receiver as live — a carrier parked there is equally excluded from the active
|
|
334
|
+
* count, so it needs the same justification.
|
|
335
|
+
*/
|
|
336
|
+
export function liveCarriers(root: string): string[] {
|
|
337
|
+
const out: string[] = [];
|
|
338
|
+
for (const sub of LIVE_SUBDIRS) {
|
|
339
|
+
const abs = path.join(root, 'agents', 'roadmaps', sub);
|
|
340
|
+
let entries: string[];
|
|
341
|
+
try {
|
|
342
|
+
entries = fs.readdirSync(abs);
|
|
343
|
+
} catch {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
for (const name of entries.sort()) {
|
|
347
|
+
if (!name.endsWith('.md') || EXCLUDE_NAMES.has(name)) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const full = path.join(abs, name);
|
|
351
|
+
let text: string;
|
|
352
|
+
try {
|
|
353
|
+
if (!fs.statSync(full).isFile()) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
text = fs.readFileSync(full, 'utf-8');
|
|
357
|
+
} catch {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (declaresCarrier(text)) {
|
|
361
|
+
out.push(path.posix.join('agents/roadmaps', sub, name));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Validate every carry annotation in the dead directories, then every live
|
|
370
|
+
* carrier against the destinations those annotations actually name.
|
|
371
|
+
*
|
|
372
|
+
* @param walked the dead-roadmap list, when the caller already has it. The walk
|
|
373
|
+
* `statSync`s every file in the archive, so taking it as a parameter is what
|
|
374
|
+
* stops the run doing that twice — once for the ledger's plan and once here.
|
|
375
|
+
* @returns the problems found, how many dead roadmaps were read, and how many
|
|
376
|
+
* live carriers were checked.
|
|
377
|
+
*/
|
|
378
|
+
export function auditCarries(
|
|
379
|
+
root: string,
|
|
380
|
+
walked: readonly string[] = deadRoadmaps(root),
|
|
381
|
+
): { problems: CarrierProblem[]; scanned: number; carriers: string[] } {
|
|
382
|
+
const problems: CarrierProblem[] = [];
|
|
383
|
+
let scanned = 0;
|
|
384
|
+
/** Every slug some dead roadmap named with `carried-to=`. */
|
|
385
|
+
const carriedTo = new Set<string>();
|
|
386
|
+
for (const rel of walked) {
|
|
387
|
+
let text: string;
|
|
388
|
+
try {
|
|
389
|
+
text = fs.readFileSync(path.join(root, rel), 'utf-8');
|
|
390
|
+
} catch {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
scanned += 1;
|
|
394
|
+
if (!CARRY_CANDIDATE_RE.test(text)) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const items = parseDeferredItems(text);
|
|
398
|
+
for (const item of items) {
|
|
399
|
+
if (item.kind === 'carried-to' && item.destination !== null) {
|
|
400
|
+
carriedTo.add(item.destination);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const sourceSlug = path.basename(rel).replace(/\.md$/, '');
|
|
404
|
+
for (const found of carryProblemsFor(root, sourceSlug, items)) {
|
|
405
|
+
problems.push({ source: rel, ...found });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const carriers = liveCarriers(root);
|
|
409
|
+
for (const rel of carriers) {
|
|
410
|
+
const slug = path.basename(rel).replace(/\.md$/, '');
|
|
411
|
+
if (carriedTo.has(slug)) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
problems.push({
|
|
415
|
+
source: rel,
|
|
416
|
+
cls: 'broken-destination',
|
|
417
|
+
detail:
|
|
418
|
+
'declares `status: carrier`, but no roadmap under `agents/roadmaps/archive/` or ' +
|
|
419
|
+
'`agents/roadmaps/skipped/` names it with a `<!-- deferred-resolution: ' +
|
|
420
|
+
`carried-to=${slug} -->\` annotation. That status is only legitimate for the ` +
|
|
421
|
+
'RECEIVER of a real carry: it buys exclusion from the active roadmap count, from ' +
|
|
422
|
+
'`check_roadmap_trackable` and from the plan risk register, so a file no archived ' +
|
|
423
|
+
'parent carried anything to would take all three exclusions for nothing. Either ' +
|
|
424
|
+
'record the carry in the parent that deferred the work, or drop `status: carrier`.',
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return { problems, scanned, carriers };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const USAGE = `usage: lint_carrier_integrity [--root <dir>] [--self-test] [--quiet]
|
|
431
|
+
|
|
432
|
+
--root <dir> audit another checkout (default: this repository)
|
|
433
|
+
--self-test run the built-in discrimination suite and exit
|
|
434
|
+
--quiet suppress the green summary line
|
|
435
|
+
`;
|
|
436
|
+
|
|
437
|
+
function _writeFixture(dir: string, rel: string, body: string): void {
|
|
438
|
+
const full = path.join(dir, rel);
|
|
439
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
440
|
+
fs.writeFileSync(full, body, 'utf-8');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const PARENT_WITH_CARRY = '---\ncomplexity: bounded\n---\n# Parent\n\n## Phase 1 - a phase\n\n- [~] **1.1 A carried step.**\n <!-- deferred-resolution: carried-to=road-to-receiver -->\n';
|
|
444
|
+
|
|
445
|
+
function _receiver(backlink: string): string {
|
|
446
|
+
return `---
|
|
447
|
+
complexity: bounded
|
|
448
|
+
${backlink}
|
|
449
|
+
---
|
|
450
|
+
# Receiver
|
|
451
|
+
|
|
452
|
+
Body.
|
|
453
|
+
`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* A body that DOCUMENTS roadmap syntax inside a fence. Both structural tests
|
|
458
|
+
* used to match it, so this fixture is the one that fails in two ways at once.
|
|
459
|
+
*/
|
|
460
|
+
const FENCED_SYNTAX_DOC = 'Syntax this file documents rather than declares:\n\n```markdown\n---\nstatus: carrier\n---\n- [ ] **1.1 An example open step.**\n```\n';
|
|
461
|
+
|
|
462
|
+
/** A back-link that exists only in the body — a mention, never a declaration. */
|
|
463
|
+
const FENCED_BODY_BACKLINK = 'What a back-link looks like, quoted:\n\n```yaml\nparent_roadmap: road-to-parent\nrelates:\n - slug: road-to-parent\n relation: extends\n```\n';
|
|
464
|
+
|
|
465
|
+
/** A receiver that declares itself a carrier — used only where that is the point. */
|
|
466
|
+
function _carrierReceiver(backlink: string): string {
|
|
467
|
+
return _receiver(backlink).replace('---\n# Receiver', 'status: carrier\n---\n# Receiver');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Each case builds a throwaway tree, runs the real CLI against it with
|
|
472
|
+
* `--root`, and asserts the exit code. Shelling out rather than calling
|
|
473
|
+
* `auditCarries` directly keeps argv parsing and the exit-code mapping inside
|
|
474
|
+
* what is proven.
|
|
475
|
+
*
|
|
476
|
+
* Every case here is a `broken-destination` case, because that is the class
|
|
477
|
+
* this CLI hard-fails on. The `unannotated` class cannot be proven by exit code
|
|
478
|
+
* at all: it is ratcheted against a baseline of 243, so one planted fixture
|
|
479
|
+
* finding sits far below the floor and correctly exits 0. Its detection is
|
|
480
|
+
* proven instead by a unit test over `carryProblems`, which asserts the class
|
|
481
|
+
* directly, plus by the 243 it reports against the real tree on every run.
|
|
482
|
+
*/
|
|
483
|
+
export function selfTestCases(root: string): SelfTestCase[] {
|
|
484
|
+
const build = (name: string, plant: (dir: string) => void): (() => number) => {
|
|
485
|
+
return () => {
|
|
486
|
+
const dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), name));
|
|
487
|
+
try {
|
|
488
|
+
plant(dir);
|
|
489
|
+
return runGateCli(
|
|
490
|
+
root,
|
|
491
|
+
'src/scripts/lint_carrier_integrity.ts',
|
|
492
|
+
['--root', dir, '--quiet'],
|
|
493
|
+
root,
|
|
494
|
+
);
|
|
495
|
+
} finally {
|
|
496
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
};
|
|
500
|
+
const parent = (dir: string): void =>
|
|
501
|
+
_writeFixture(dir, 'agents/roadmaps/archive/road-to-parent.md', PARENT_WITH_CARRY);
|
|
502
|
+
return [
|
|
503
|
+
{
|
|
504
|
+
name: 'a live receiver with a back-link is accepted',
|
|
505
|
+
expect: 'accept',
|
|
506
|
+
run: build('carrier-ok-', (dir) => {
|
|
507
|
+
parent(dir);
|
|
508
|
+
_writeFixture(
|
|
509
|
+
dir,
|
|
510
|
+
'agents/roadmaps/road-to-receiver.md',
|
|
511
|
+
_receiver('parent_roadmap: road-to-parent'),
|
|
512
|
+
);
|
|
513
|
+
}),
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: 'a receiver parked under later/ is equally live and is accepted',
|
|
517
|
+
expect: 'accept',
|
|
518
|
+
run: build('carrier-later-', (dir) => {
|
|
519
|
+
parent(dir);
|
|
520
|
+
_writeFixture(
|
|
521
|
+
dir,
|
|
522
|
+
'agents/roadmaps/later/road-to-receiver.md',
|
|
523
|
+
_receiver('parent_roadmap: road-to-parent'),
|
|
524
|
+
);
|
|
525
|
+
}),
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
name: 'a receiver that completed its work and archived is accepted — fulfilment, not loss',
|
|
529
|
+
expect: 'accept',
|
|
530
|
+
run: build('carrier-done-', (dir) => {
|
|
531
|
+
parent(dir);
|
|
532
|
+
_writeFixture(
|
|
533
|
+
dir,
|
|
534
|
+
'agents/roadmaps/archive/road-to-receiver.md',
|
|
535
|
+
`${_receiver('parent_roadmap: road-to-parent')}\n- [x] **1.1 Done.**\n`,
|
|
536
|
+
);
|
|
537
|
+
}),
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
name: 'a back-link supplied as a relates: row instead of parent_roadmap is accepted',
|
|
541
|
+
expect: 'accept',
|
|
542
|
+
run: build('carrier-relates-', (dir) => {
|
|
543
|
+
parent(dir);
|
|
544
|
+
_writeFixture(
|
|
545
|
+
dir,
|
|
546
|
+
'agents/roadmaps/road-to-receiver.md',
|
|
547
|
+
_receiver('relates:\n - slug: road-to-parent\n relation: extends'),
|
|
548
|
+
);
|
|
549
|
+
}),
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
name: 'a live carrier an archived parent actually names is accepted',
|
|
553
|
+
expect: 'accept',
|
|
554
|
+
run: build('carrier-named-', (dir) => {
|
|
555
|
+
parent(dir);
|
|
556
|
+
_writeFixture(
|
|
557
|
+
dir,
|
|
558
|
+
'agents/roadmaps/road-to-receiver.md',
|
|
559
|
+
_carrierReceiver('parent_roadmap: road-to-parent'),
|
|
560
|
+
);
|
|
561
|
+
}),
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: 'an archived receiver that only documents roadmap syntax in a fence is accepted',
|
|
565
|
+
expect: 'accept',
|
|
566
|
+
run: build('carrier-fenced-', (dir) => {
|
|
567
|
+
parent(dir);
|
|
568
|
+
_writeFixture(
|
|
569
|
+
dir,
|
|
570
|
+
'agents/roadmaps/archive/road-to-receiver.md',
|
|
571
|
+
`${_receiver('parent_roadmap: road-to-parent')}\n${FENCED_SYNTAX_DOC}`,
|
|
572
|
+
);
|
|
573
|
+
}),
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
name: 'a live carrier no archived parent names is rejected — the status is not free',
|
|
577
|
+
expect: 'reject',
|
|
578
|
+
run: build('carrier-orphan-', (dir) => {
|
|
579
|
+
parent(dir);
|
|
580
|
+
_writeFixture(
|
|
581
|
+
dir,
|
|
582
|
+
'agents/roadmaps/road-to-receiver.md',
|
|
583
|
+
_receiver('parent_roadmap: road-to-parent'),
|
|
584
|
+
);
|
|
585
|
+
_writeFixture(
|
|
586
|
+
dir,
|
|
587
|
+
'agents/roadmaps/road-to-self-declared.md',
|
|
588
|
+
_carrierReceiver('owner: maintainer'),
|
|
589
|
+
);
|
|
590
|
+
}),
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
name: 'an orphan carrier parked under later/ is rejected too',
|
|
594
|
+
expect: 'reject',
|
|
595
|
+
run: build('carrier-orphan-later-', (dir) => {
|
|
596
|
+
parent(dir);
|
|
597
|
+
_writeFixture(
|
|
598
|
+
dir,
|
|
599
|
+
'agents/roadmaps/road-to-receiver.md',
|
|
600
|
+
_receiver('parent_roadmap: road-to-parent'),
|
|
601
|
+
);
|
|
602
|
+
_writeFixture(
|
|
603
|
+
dir,
|
|
604
|
+
'agents/roadmaps/later/road-to-self-declared.md',
|
|
605
|
+
_carrierReceiver('owner: maintainer'),
|
|
606
|
+
);
|
|
607
|
+
}),
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
name: 'an archived receiver with an asterisk-bulleted OPEN step is rejected',
|
|
611
|
+
expect: 'reject',
|
|
612
|
+
run: build('carrier-asterisk-', (dir) => {
|
|
613
|
+
parent(dir);
|
|
614
|
+
_writeFixture(
|
|
615
|
+
dir,
|
|
616
|
+
'agents/roadmaps/archive/road-to-receiver.md',
|
|
617
|
+
`${_receiver('parent_roadmap: road-to-parent')}\n* [ ] **1.1 Still open.**\n`,
|
|
618
|
+
);
|
|
619
|
+
}),
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
name: 'a back-link that only appears in the body, not the frontmatter, is rejected',
|
|
623
|
+
expect: 'reject',
|
|
624
|
+
run: build('carrier-bodylink-', (dir) => {
|
|
625
|
+
parent(dir);
|
|
626
|
+
_writeFixture(
|
|
627
|
+
dir,
|
|
628
|
+
'agents/roadmaps/road-to-receiver.md',
|
|
629
|
+
`${_receiver('owner: maintainer')}\n${FENCED_BODY_BACKLINK}`,
|
|
630
|
+
);
|
|
631
|
+
}),
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: 'a receiver that is itself a carrier and has been archived is rejected',
|
|
635
|
+
expect: 'reject',
|
|
636
|
+
run: build('carrier-archived-carrier-', (dir) => {
|
|
637
|
+
parent(dir);
|
|
638
|
+
_writeFixture(
|
|
639
|
+
dir,
|
|
640
|
+
'agents/roadmaps/archive/road-to-receiver.md',
|
|
641
|
+
_carrierReceiver('parent_roadmap: road-to-parent'),
|
|
642
|
+
);
|
|
643
|
+
}),
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
name: 'the receiver deleted entirely is rejected',
|
|
647
|
+
expect: 'reject',
|
|
648
|
+
run: build('carrier-deleted-', parent),
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
name: 'a receiver archived while still carrying OPEN steps is rejected — stranded, not completed',
|
|
652
|
+
expect: 'reject',
|
|
653
|
+
run: build('carrier-stranded-', (dir) => {
|
|
654
|
+
parent(dir);
|
|
655
|
+
_writeFixture(
|
|
656
|
+
dir,
|
|
657
|
+
'agents/roadmaps/archive/road-to-receiver.md',
|
|
658
|
+
`${_receiver('parent_roadmap: road-to-parent')}\n- [ ] **1.1 Still open.**\n`,
|
|
659
|
+
);
|
|
660
|
+
}),
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
name: 'a receiver under skipped/ is rejected — skipping is not fulfilling',
|
|
664
|
+
expect: 'reject',
|
|
665
|
+
run: build('carrier-skipped-', (dir) => {
|
|
666
|
+
parent(dir);
|
|
667
|
+
_writeFixture(
|
|
668
|
+
dir,
|
|
669
|
+
'agents/roadmaps/skipped/road-to-receiver.md',
|
|
670
|
+
_receiver('parent_roadmap: road-to-parent'),
|
|
671
|
+
);
|
|
672
|
+
}),
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
name: 'the back-link removed is rejected — the link must verify from both ends',
|
|
676
|
+
expect: 'reject',
|
|
677
|
+
run: build('carrier-nolink-', (dir) => {
|
|
678
|
+
parent(dir);
|
|
679
|
+
_writeFixture(
|
|
680
|
+
dir,
|
|
681
|
+
'agents/roadmaps/road-to-receiver.md',
|
|
682
|
+
_receiver('owner: maintainer'),
|
|
683
|
+
);
|
|
684
|
+
}),
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
name: 'a back-link naming a different parent is rejected',
|
|
688
|
+
expect: 'reject',
|
|
689
|
+
run: build('carrier-reparent-', (dir) => {
|
|
690
|
+
parent(dir);
|
|
691
|
+
_writeFixture(
|
|
692
|
+
dir,
|
|
693
|
+
'agents/roadmaps/road-to-receiver.md',
|
|
694
|
+
_receiver('parent_roadmap: road-to-someone-else'),
|
|
695
|
+
);
|
|
696
|
+
}),
|
|
697
|
+
},
|
|
698
|
+
];
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export function main(argv: string[]): number {
|
|
702
|
+
let root = _DEFAULT_ROOT;
|
|
703
|
+
let quiet = false;
|
|
704
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
705
|
+
const arg = argv[i] as string;
|
|
706
|
+
if (arg === '-h' || arg === '--help') {
|
|
707
|
+
process.stdout.write(USAGE);
|
|
708
|
+
return 0;
|
|
709
|
+
} else if (arg === '--quiet') {
|
|
710
|
+
quiet = true;
|
|
711
|
+
} else if (arg === '--root') {
|
|
712
|
+
const next = argv[i + 1];
|
|
713
|
+
if (next === undefined) {
|
|
714
|
+
process.stderr.write('lint_carrier_integrity: --root needs a directory\n');
|
|
715
|
+
return 2;
|
|
716
|
+
}
|
|
717
|
+
root = path.resolve(next);
|
|
718
|
+
i += 1;
|
|
719
|
+
} else if (arg === '--self-test') {
|
|
720
|
+
return runSelfTest({
|
|
721
|
+
gate: 'lint_carrier_integrity',
|
|
722
|
+
cases: selfTestCases(_DEFAULT_ROOT),
|
|
723
|
+
minCases: 16,
|
|
724
|
+
minRejectCases: 10,
|
|
725
|
+
});
|
|
726
|
+
} else {
|
|
727
|
+
process.stderr.write(`lint_carrier_integrity: unknown argument ${JSON.stringify(arg)}\n`);
|
|
728
|
+
process.stderr.write(USAGE);
|
|
729
|
+
return 2;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const ledger = new GateLedger('lint_carrier_integrity');
|
|
734
|
+
const walked = deadRoadmaps(root);
|
|
735
|
+
// Walked ONCE and handed to the audit. The two enumerations used to each
|
|
736
|
+
// call `deadRoadmaps`, so a run cost two readdirs and two `statSync` per
|
|
737
|
+
// archived file for one reading.
|
|
738
|
+
const { problems, scanned, carriers } = auditCarries(root, walked);
|
|
739
|
+
ledger.plan([...walked, ...carriers]);
|
|
740
|
+
const failed = new Set(problems.map((p) => p.source));
|
|
741
|
+
for (const rel of [...walked, ...carriers]) {
|
|
742
|
+
if (failed.has(rel)) {
|
|
743
|
+
ledger.fail(rel, problems.find((p) => p.source === rel)?.detail ?? 'broken carry');
|
|
744
|
+
} else {
|
|
745
|
+
ledger.complete(rel);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
reportScanned({
|
|
750
|
+
gate: 'lint_carrier_integrity',
|
|
751
|
+
scanned,
|
|
752
|
+
units: 'archived roadmap(s)',
|
|
753
|
+
roots: DEAD_DIRS.map((d) => `agents/roadmaps/${d}`),
|
|
754
|
+
allowEmpty: 'a checkout with no archived roadmaps has no carry to break',
|
|
755
|
+
});
|
|
756
|
+
} catch (e) {
|
|
757
|
+
if (e instanceof DeadScopeError) {
|
|
758
|
+
process.stderr.write(`${e.message}\n`);
|
|
759
|
+
ledger.report();
|
|
760
|
+
return 1;
|
|
761
|
+
}
|
|
762
|
+
throw e;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const broken = problems.filter((p) => p.cls === 'broken-destination');
|
|
766
|
+
const unannotated = problems.filter((p) => p.cls === 'unannotated');
|
|
767
|
+
|
|
768
|
+
// Hard failure, at zero, no baseline. Every member of this class is an
|
|
769
|
+
// obligation that had a recorded destination and lost it.
|
|
770
|
+
if (broken.length > 0) {
|
|
771
|
+
process.stderr.write(
|
|
772
|
+
`\n❌ ${String(broken.length)} broken deferral carr${broken.length === 1 ? 'y' : 'ies'}. ` +
|
|
773
|
+
'An archived roadmap named a receiver that no longer holds up its end, so the ' +
|
|
774
|
+
'obligations it carried have no live destination.\n\n',
|
|
775
|
+
);
|
|
776
|
+
for (const p of broken) {
|
|
777
|
+
process.stderr.write(` ❌ ${p.source}\n ${p.detail}\n`);
|
|
778
|
+
}
|
|
779
|
+
process.stderr.write(
|
|
780
|
+
'\n Restore the receiver, restore its `parent_roadmap:` back-link, or move the\n' +
|
|
781
|
+
' carried items back to open in the archived parent. A rename or a re-parent is\n' +
|
|
782
|
+
' not expressible yet and fails closed on purpose.\n' +
|
|
783
|
+
' A file listed for declaring `status: carrier` needs neither: record the carry\n' +
|
|
784
|
+
' in the archived parent, or drop the status and let the file be ordinary estate.\n',
|
|
785
|
+
);
|
|
786
|
+
ledger.report();
|
|
787
|
+
return 1;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const verdict = checkRatchet({
|
|
791
|
+
gate: 'lint_carrier_integrity',
|
|
792
|
+
actual: unannotated.length,
|
|
793
|
+
repoRoot: _DEFAULT_ROOT,
|
|
794
|
+
});
|
|
795
|
+
if (!verdict.ok) {
|
|
796
|
+
process.stderr.write(`\n❌ ${verdict.message}\n\n`);
|
|
797
|
+
for (const p of unannotated.slice(0, 20)) {
|
|
798
|
+
process.stderr.write(` ❌ ${p.source}\n ${p.detail}\n`);
|
|
799
|
+
}
|
|
800
|
+
if (unannotated.length > 20) {
|
|
801
|
+
process.stderr.write(` … and ${String(unannotated.length - 20)} more.\n`);
|
|
802
|
+
}
|
|
803
|
+
ledger.report();
|
|
804
|
+
return 1;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (!quiet) {
|
|
808
|
+
process.stdout.write(`✅ ${verdict.message}\n`);
|
|
809
|
+
process.stdout.write(
|
|
810
|
+
`✅ lint_carrier_integrity: ${String(scanned)} dead roadmap(s) scanned, ` +
|
|
811
|
+
`${String(carriers.length)} live carrier(s) justified, every annotated carry resolves.\n`,
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
ledger.report();
|
|
815
|
+
return 0;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
819
|
+
process.exit(main(process.argv.slice(2)));
|
|
820
|
+
}
|