@event4u/agent-config 14.21.0 → 14.22.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 +198 -0
- package/CREDITS.md +37 -0
- package/dist/agent-src/commands/analyze/inbox.md +380 -5
- package/dist/agent-src/commands/analyze/repo.md +4 -2
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
- package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
- package/dist/agent-src/rules/token-efficiency.md +26 -3
- package/dist/agent-src/scripts/_cli_entry.ts +51 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
- package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/dist/agent-src/scripts/stubs_due.ts +4 -18
- package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
- package/dist/agent-src/templates/agent-settings.md +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/_cli_entry.js +8 -0
- package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
- package/dist/cli-delegate/archival_sweep.js +8 -0
- package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
- package/dist/cli-delegate/check_update_banner.js +923 -0
- package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
- package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
- package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-BIXEDQPZ.js} +6 -0
- package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
- package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
- package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
- package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
- package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
- package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
- package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
- package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
- package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
- package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +26 -2
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/cli-delegate/dashboard_mode.js +16 -0
- package/dist/cli-delegate/gate_budget.js +24 -0
- package/dist/cli-delegate/gate_execute.js +25 -0
- package/dist/cli-delegate/guarded_baseline.js +18 -0
- package/dist/cli-delegate/resume_probe.js +22 -0
- package/dist/cli-delegate/roadmap_gates.js +638 -0
- package/dist/cli-delegate/roadmap_set_step.js +173 -0
- package/dist/cli-delegate/stub_queue.js +47 -0
- package/dist/cli-delegate/stubs_due.js +38 -0
- package/dist/cli-delegate/ticket_bundles.js +59 -0
- package/dist/cli-delegate/update_roadmap_progress.js +60 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +8 -8
- 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 +1193 -917
- package/dist/install/claudeLayerCarriage.js +237 -0
- package/dist/install/claudePathsPlan.js +79 -0
- package/dist/install/hostLayerFingerprint.js +1 -1
- package/dist/install/install.mjs +6 -0
- package/dist/install/partitionEligibility.js +231 -117
- package/dist/install/ruleLayerPartition.js +10 -7
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +39219 -38995
- package/dist/scripts/condense.js +32 -25
- package/docs/MIGRATION.md +49 -1
- package/docs/contracts/settings-classes.md +1 -1
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
- package/package.json +7 -4
- package/src/agent-src/scripts/_cli_entry.ts +51 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/src/agent-src/scripts/roadmap_gates.ts +4 -20
- package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/src/agent-src/scripts/stubs_due.ts +4 -18
- package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/src/config/continuity-surface.json +509 -0
- package/src/config/estate-count-budget.json +1 -1
- package/src/config/gate-coverage.yml +29 -8
- package/src/config/gate-violation-baselines.json +8 -8
- package/src/config/gitignore-block.txt +19 -0
- package/src/config/host-injection-effect.json +73 -0
- package/src/config/packed-binary-manifest.json +33 -0
- package/src/config/preamble-payload-budget.json +112 -105
- package/src/config/publish-surface.json +4 -2
- package/src/install/claudeLayerCarriage.ts +273 -0
- package/src/install/claudePathsPlan.ts +76 -0
- package/src/install/hostLayerFingerprint.ts +1 -1
- package/src/install/partitionEligibility.ts +261 -127
- package/src/install/ruleLayerPartition.ts +19 -23
- package/src/scripts/_dispatch.bash +77 -27
- package/src/scripts/_lib/auto_dispatch.ts +2 -2
- package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
- package/src/scripts/_lib/continuity_surface.ts +296 -0
- package/src/scripts/_lib/corpus_manifest.ts +26 -9
- package/src/scripts/_lib/hook_settings.ts +1 -1
- package/src/scripts/_lib/injection_effect.ts +151 -0
- package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
- package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
- package/src/scripts/_lib/pin_resolver.ts +13 -0
- package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
- package/src/scripts/_lib/retired_status.ts +74 -0
- package/src/scripts/_lib/skill_catalogue.ts +156 -21
- package/src/scripts/_lib/subagent_capsule.ts +117 -8
- package/src/scripts/ai_council/api_transport.ts +5 -0
- package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
- package/src/scripts/ai_council/prompts.ts +9 -0
- package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
- package/src/scripts/capture_skill_catalogue.ts +24 -11
- package/src/scripts/check_candidate_lines.ts +499 -0
- package/src/scripts/check_continuity_surface.ts +475 -0
- package/src/scripts/check_dependency_floors.ts +13 -6
- package/src/scripts/check_estate_count.ts +40 -79
- package/src/scripts/check_pack_size.ts +262 -23
- package/src/scripts/check_roadmap_trackable.ts +19 -16
- package/src/scripts/check_rule_layer_partition.ts +54 -34
- package/src/scripts/check_single_delivery.ts +97 -31
- package/src/scripts/check_update_banner.ts +13 -0
- package/src/scripts/code_graph/build.ts +138 -15
- package/src/scripts/code_graph/cli.ts +76 -8
- package/src/scripts/code_graph/extract.ts +11 -0
- package/src/scripts/code_graph/grammar_probe.ts +43 -0
- package/src/scripts/code_graph/loader.ts +88 -14
- package/src/scripts/code_graph/query.ts +121 -11
- package/src/scripts/code_graph/resolution_tiers.ts +238 -0
- package/src/scripts/code_graph/sqlite_store.ts +247 -6
- package/src/scripts/code_graph/types.ts +66 -1
- package/src/scripts/code_graph/validate.ts +23 -0
- package/src/scripts/condense.ts +34 -22
- package/src/scripts/corpus_manifest.ts +5 -5
- package/src/scripts/council_cli.ts +11 -12
- package/src/scripts/count_intent_disagreement.ts +426 -0
- package/src/scripts/handoff_context_hook.ts +79 -0
- package/src/scripts/hook_effect_doctor.ts +82 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +64 -12
- package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
- package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
- package/src/scripts/hooks/concern_registry.ts +4 -2
- package/src/scripts/hooks/host_semantics.ts +1 -1
- package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
- package/src/scripts/hooks/rule_inject_hook.ts +1 -1
- package/src/scripts/hooks/skill_route_hook.ts +11 -4
- package/src/scripts/inbox_source_census.ts +322 -0
- package/src/scripts/install-hooks.sh +82 -0
- package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
- package/src/scripts/lint_plan_risk_register.ts +13 -17
- package/src/scripts/mcp_server/suggest_skill.ts +72 -0
- package/src/scripts/mcp_server/tools.ts +2 -56
- package/src/scripts/media/lib/adapter-contract.md +7 -2
- package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
- package/src/scripts/rdp_candidates_delta.ts +276 -0
- package/src/scripts/rdp_quality_eval.ts +217 -19
- package/src/scripts/release_mix_taxonomy.json +3 -2
- package/src/scripts/report_host_injection_effect.ts +189 -0
- package/src/scripts/report_layer_overlap.ts +41 -0
- package/src/scripts/report_skill_menu_census.ts +341 -0
- package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
- package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
- package/src/vendor/grammars/README.md +61 -0
- package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
- package/src/scripts/_lib/carrier_status.ts +0 -118
- package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
- package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
|
@@ -155,8 +155,6 @@ import { materialiseSubtree } from './_lib/base_tree.js';
|
|
|
155
155
|
import { SKILLS_POSIX, measureSkillEstate } from './_lib/skill_estate.js';
|
|
156
156
|
import { CONCERN_MANIFEST_POSIX, countConcerns } from './_lib/concern_estate.js';
|
|
157
157
|
import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
|
|
158
|
-
import { countDeclaredCarriers, declaresCarrier } from './_lib/carrier_status.js';
|
|
159
|
-
import { carrierEstateCases } from './_lib/estate_carrier_cases.js';
|
|
160
158
|
|
|
161
159
|
const GATE = 'check_estate_count';
|
|
162
160
|
const BUDGET_REL = path.join('src', 'config', 'estate-count-budget.json');
|
|
@@ -438,38 +436,12 @@ function countIn(roadmapRoot: string, sub: string): number {
|
|
|
438
436
|
}
|
|
439
437
|
}
|
|
440
438
|
|
|
441
|
-
/**
|
|
442
|
-
* Top-level roadmaps `collect()` drops for declaring `status: carrier`, added
|
|
443
|
-
* back so that FLIPPING a status is count-neutral.
|
|
444
|
-
*
|
|
445
|
-
* Measured before this: flipping one file to `carrier` moved `active_roadmaps
|
|
446
|
-
* 3 → 2` and the gate printed "estate within its ratchet", because a count
|
|
447
|
-
* below the floor is a drawdown and free. The floor is measured at the base
|
|
448
|
-
* ref, so once such a flip merges the lowered number becomes the next floor —
|
|
449
|
-
* the file cannot be flipped back without paying, and any roadmap can be
|
|
450
|
-
* laundered out of the count by adding one word.
|
|
451
|
-
*
|
|
452
|
-
* A reclassification is not a disposal, so the metric counts the file on both
|
|
453
|
-
* sides. Same argument `open_blockers` makes below for spanning `later/`.
|
|
454
|
-
* Deleting a carrier is still a shrink and still earns no offset
|
|
455
|
-
* (`classifyDiff`); adding one is growth that takes the claim path.
|
|
456
|
-
*
|
|
457
|
-
* This is therefore the one roadmap metric that does NOT equal the dashboard
|
|
458
|
-
* header, stated here rather than left to be tripped over.
|
|
459
|
-
*/
|
|
460
|
-
function countActiveCarriers(roadmapRoot: string): number {
|
|
461
|
-
return countDeclaredCarriers(roadmapRoot, isRoadmapCandidate, fs, path.join);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
439
|
/**
|
|
465
440
|
* Roadmaps parked in `later/` whose blockers count, parsed for them.
|
|
466
441
|
*
|
|
467
|
-
* Skips
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
* them out — a ratchet verdict that moved on a relocation, for no stated
|
|
471
|
-
* reason. Three of the four status readers learned `carrier` when the status
|
|
472
|
-
* shipped; this is the fourth.
|
|
442
|
+
* Skips exactly what `collect()` skips on the active side, through the same
|
|
443
|
+
* `isUnscheduled` predicate. Skipping a different set here would make a ratchet
|
|
444
|
+
* verdict move on a relocation, for no stated reason.
|
|
473
445
|
*/
|
|
474
446
|
function laterRoadmaps(roadmapRoot: string): Array<{ open_blockers: readonly unknown[] }> {
|
|
475
447
|
const dir = path.join(roadmapRoot, 'later');
|
|
@@ -496,10 +468,13 @@ function laterRoadmaps(roadmapRoot: string): Array<{ open_blockers: readonly unk
|
|
|
496
468
|
/**
|
|
497
469
|
* Count the estate, three ways.
|
|
498
470
|
*
|
|
499
|
-
* `active_roadmaps` is `collect()
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
471
|
+
* `active_roadmaps` is `collect()`, so the parser is shared with the dashboard
|
|
472
|
+
* and the number equals the dashboard header. It used to carry an add-back for
|
|
473
|
+
* the top-level roadmaps `collect()` dropped for declaring `status: carrier`,
|
|
474
|
+
* which existed only to keep FLIPPING to that status count-neutral — a flip
|
|
475
|
+
* lowered the count, a count below the floor is a free drawdown, and the
|
|
476
|
+
* lowered number then became the next floor. ADR-262 deleted the status, so
|
|
477
|
+
* there is no flip to neutralise and no divergence from the header left.
|
|
503
478
|
* `later_roadmaps` is a directory listing because parked files are
|
|
504
479
|
* outside that corpus by design — filtered through the same `is_roadmap_candidate`
|
|
505
480
|
* predicate as the active side, so `later/README.md` is not a roadmap here either.
|
|
@@ -524,7 +499,7 @@ export function countEstate(repoRoot: string): EstateCounts {
|
|
|
524
499
|
rows.reduce((n, r) => n + r.open_blockers.length, 0);
|
|
525
500
|
const skills = measureSkillEstate(repoRoot);
|
|
526
501
|
return {
|
|
527
|
-
active_roadmaps: stats.length
|
|
502
|
+
active_roadmaps: stats.length,
|
|
528
503
|
later_roadmaps: countIn(roadmapRoot, 'later'),
|
|
529
504
|
open_blockers: openOf(stats) + openOf(laterRoadmaps(roadmapRoot)),
|
|
530
505
|
skill_count: skills.skill_count,
|
|
@@ -590,9 +565,6 @@ export function exemptionReason(text: string): string | null {
|
|
|
590
565
|
return reason === '' ? null : reason;
|
|
591
566
|
}
|
|
592
567
|
|
|
593
|
-
/** Re-export; `_lib/carrier_status.ts` holds the one implementation. */
|
|
594
|
-
export const isCarrierText = declaresCarrier;
|
|
595
|
-
|
|
596
568
|
/**
|
|
597
569
|
* Classify the change's effect on the active roadmap tree.
|
|
598
570
|
*
|
|
@@ -604,13 +576,6 @@ export const isCarrierText = declaresCarrier;
|
|
|
604
576
|
export function classifyDiff(
|
|
605
577
|
nameStatus: string,
|
|
606
578
|
readFile: (rel: string) => string | null,
|
|
607
|
-
/**
|
|
608
|
-
* The file's content at the BASE ref, for paths this change removed — a
|
|
609
|
-
* deleted carrier cannot be read from the working tree and its status
|
|
610
|
-
* decides whether the deletion earns an offset. Defaults to null, which
|
|
611
|
-
* reproduces the previous scoring for any caller without a pre-image.
|
|
612
|
-
*/
|
|
613
|
-
readBase: (rel: string) => string | null = () => null,
|
|
614
579
|
): OffsetLedger {
|
|
615
580
|
const added: string[] = [];
|
|
616
581
|
const offsets: string[] = [];
|
|
@@ -624,10 +589,8 @@ export function classifyDiff(
|
|
|
624
589
|
const from = cols[1] ?? '';
|
|
625
590
|
const to = cols[2] ?? '';
|
|
626
591
|
if (isActiveTopLevel(from) && isDisposed(to)) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (isParked(to)) parked.push(to);
|
|
630
|
-
}
|
|
592
|
+
offsets.push(from);
|
|
593
|
+
if (isParked(to)) parked.push(to);
|
|
631
594
|
} else if (isDisposed(from) && isActiveTopLevel(to)) {
|
|
632
595
|
added.push(to);
|
|
633
596
|
}
|
|
@@ -638,10 +601,7 @@ export function classifyDiff(
|
|
|
638
601
|
if (status === 'A') {
|
|
639
602
|
added.push(file);
|
|
640
603
|
} else if (status === 'D') {
|
|
641
|
-
|
|
642
|
-
if (!isCarrierText(readBase(file))) {
|
|
643
|
-
offsets.push(file);
|
|
644
|
-
}
|
|
604
|
+
offsets.push(file);
|
|
645
605
|
}
|
|
646
606
|
}
|
|
647
607
|
for (const file of added) {
|
|
@@ -832,20 +792,13 @@ export function evaluate(
|
|
|
832
792
|
if (!diff.ok) {
|
|
833
793
|
offsetSkipReason = `git diff against ${baseRef} failed — one-in-one-out not evaluated`;
|
|
834
794
|
} else {
|
|
835
|
-
offsets = classifyDiff(
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
}
|
|
843
|
-
},
|
|
844
|
-
(rel) => {
|
|
845
|
-
const show = git(['show', `${baseRef}:${rel}`], repoRoot);
|
|
846
|
-
return show.ok ? show.stdout : null;
|
|
847
|
-
},
|
|
848
|
-
);
|
|
795
|
+
offsets = classifyDiff(diff.stdout, (rel) => {
|
|
796
|
+
try {
|
|
797
|
+
return fs.readFileSync(path.join(repoRoot, rel), 'utf-8');
|
|
798
|
+
} catch {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
});
|
|
849
802
|
}
|
|
850
803
|
const patch = git(
|
|
851
804
|
['diff', '--unified=0', '--find-renames', `${baseRef}...HEAD`, '--', ROADMAPS_REL],
|
|
@@ -982,9 +935,6 @@ function selfTest(): number {
|
|
|
982
935
|
const roadmap = (name: string, extra = ''): string =>
|
|
983
936
|
`# Roadmap: ${name}\n\n## Phase 1\n\n- [ ] **1.1** s\n${extra}`;
|
|
984
937
|
|
|
985
|
-
/** A `status: carrier` roadmap — counted as estate, and never an offset. */
|
|
986
|
-
const CARRIER = `---\nstatus: carrier\n---\n# Roadmap: carrier\n\n## Phase 1\n\n- [~] **1.1** s\n`;
|
|
987
|
-
|
|
988
938
|
/**
|
|
989
939
|
* A `status: draft` roadmap — invisible to `collect()`, so adding one cannot
|
|
990
940
|
* move `active_roadmaps`. That is what makes it the fixture for a case that
|
|
@@ -1197,14 +1147,25 @@ function selfTest(): number {
|
|
|
1197
1147
|
after: (dir) => write(dir, 'agents/roadmaps/later/road-to-parked.md', roadmap('P')),
|
|
1198
1148
|
}),
|
|
1199
1149
|
},
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1150
|
+
{
|
|
1151
|
+
// Survives the deletion of the carrier cases this used to control
|
|
1152
|
+
// for (ADR-262): it asserts the plain rule those cases were the
|
|
1153
|
+
// exception to — a removal offsets an addition — over a DRAFT
|
|
1154
|
+
// addition, which `collect()` cannot see, so only the offset
|
|
1155
|
+
// scoring can decide it.
|
|
1156
|
+
name: 'deleting an ordinary roadmap offsets a new draft → accept',
|
|
1157
|
+
expect: 'accept',
|
|
1158
|
+
run: () =>
|
|
1159
|
+
fixture({
|
|
1160
|
+
roadmaps: 3,
|
|
1161
|
+
base: 'main',
|
|
1162
|
+
before: (dir) => write(dir, 'agents/roadmaps/road-to-spare.md', roadmap('S')),
|
|
1163
|
+
after: (dir) => {
|
|
1164
|
+
fs.rmSync(path.join(dir, 'agents/roadmaps/road-to-spare.md'));
|
|
1165
|
+
write(dir, 'agents/roadmaps/road-to-new.md', DRAFT);
|
|
1166
|
+
},
|
|
1167
|
+
}),
|
|
1168
|
+
},
|
|
1208
1169
|
{
|
|
1209
1170
|
name: 'a new open blocker with no claim → reject',
|
|
1210
1171
|
expect: 'reject',
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* Exit codes: 0 green · 1 over budget · 2 misuse / unreadable input.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { execFileSync } from 'node:child_process';
|
|
25
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
26
26
|
import { createHash } from 'node:crypto';
|
|
27
27
|
import * as fs from 'node:fs';
|
|
28
28
|
import * as path from 'node:path';
|
|
@@ -30,6 +30,12 @@ import { fileURLToPath } from 'node:url';
|
|
|
30
30
|
|
|
31
31
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
32
32
|
import { runSelfTest } from './_lib/gate_self_test.js';
|
|
33
|
+
import {
|
|
34
|
+
type EligibilityVerdict,
|
|
35
|
+
evaluateEntry,
|
|
36
|
+
type GrammarProbe,
|
|
37
|
+
loadManifest,
|
|
38
|
+
} from './_lib/packed_binary_predicate.js';
|
|
33
39
|
|
|
34
40
|
const REPO_ROOT = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
|
|
35
41
|
const BUDGET_PATH = path.join(REPO_ROOT, 'src', 'config', 'pack-size-budget.json');
|
|
@@ -306,19 +312,26 @@ export function payloadFingerprint(entryPath: string, size: number): string {
|
|
|
306
312
|
}
|
|
307
313
|
|
|
308
314
|
/**
|
|
309
|
-
*
|
|
310
|
-
* text and these 15 are not. `archive` is empty and stays a hard zero.
|
|
315
|
+
* Bound exceptions exist for `dotfile` and `no-extension` ONLY.
|
|
311
316
|
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
+
* `binary` and `archive` are not exceptable HERE and never will be: this table
|
|
318
|
+
* is prose-bound, and prose is exactly what a binary must not be admitted on.
|
|
319
|
+
* `archive` remains a hard zero with no admission path of any kind. `binary`
|
|
320
|
+
* has one since 2026-09-08 — `src/config/packed-binary-manifest.json`, gated by
|
|
321
|
+
* the mechanically verified predicate in `_lib/packed_binary_predicate.ts`,
|
|
322
|
+
* which re-derives every claim from the packed bytes. Adding a `binary` row to
|
|
323
|
+
* the table below still does nothing. That is AC-6 of
|
|
324
|
+
* `road-to-scan-that-fails-closed` read literally, after an AI council
|
|
325
|
+
* (2026-09-07, 2 seats, unanimous over two rounds) resolved the split between
|
|
326
|
+
* reading "at zero" as the ratchet and as the observed count. It chose the
|
|
327
|
+
* observed count, and chose disposition 3 — ship a textual representation —
|
|
328
|
+
* over generating fixtures at dry-run time, on the measurement that 12 of the
|
|
329
|
+
* 15 asset-bearing media fixtures ALREADY ship an ASCII placeholder at their
|
|
330
|
+
* documented path (`FIXTURE-<adapter-id>-<ext>`). The three 69-byte PNGs that
|
|
331
|
+
* used to sit here were the outliers, not the norm; they now carry the sibling
|
|
332
|
+
* convention and the class is empty by observation.
|
|
317
333
|
*/
|
|
318
334
|
const BOUND_PAYLOAD_EXCEPTIONS: readonly BoundPayloadException[] = [
|
|
319
|
-
{ fingerprint: '955b5da399e14fb39cfe1f3279044061043e64e2bd1576871514db927e56d701', path: 'src/scripts/media/lib/fixtures/flux/asset-0001.png', size: 69, why: 'media-adapter response fixture — a 69-byte placeholder PNG the flux adapter test decodes; binary by construction' },
|
|
320
|
-
{ fingerprint: 'c2dd9743367ece8ffe0deef5e798d39967c22245749fd7f798edd8aa78b70f59', path: 'src/scripts/media/lib/fixtures/gemini-image/asset-0001.png', size: 69, why: 'media-adapter response fixture — same class as the flux placeholder' },
|
|
321
|
-
{ fingerprint: 'cbac44d021a1dc41b938e798eb9be4147fc9d35194c348dfe9acaa0596bc83a5', path: 'src/scripts/media/lib/fixtures/ideogram/asset-0001.png', size: 69, why: 'media-adapter response fixture — same class as the flux placeholder' },
|
|
322
335
|
{ fingerprint: '47e4328e71652a0375ade7eb4dac8a3e4b6df1c6e4e74caa75287266676cc9a6', path: 'agents/templates/.ai-council.yml.example', size: 30561, why: 'shipped council config TEMPLATE — a dotfile because the file it is copied to is one. RE-PINNED 2026-09-07, 29,872 -> 30,561 bytes: the exception is path-AND-size-bound by design, so this branch adding a `disabled_reason` honest-null block to the gemini seat in the template invalidated it and the gate refused the entry as unaccounted. The re-pin excepts the SAME path for the SAME reason at its measured new size — it does not widen the class, raise a cap, or except anything the gate was not already carrying.' },
|
|
323
336
|
{ fingerprint: 'd36a53951c184db7481139235056e90b53a853714b3a68b31ba1349ecb95ac1d', path: 'agents/templates/.ai-video.xml.example', size: 9209, why: 'shipped video-provider config template — same reason as the council template' },
|
|
324
337
|
{ fingerprint: 'f83229412cf8cceaadd0cb1934140a0c7c8277df43edc590f370572f6df6da98', path: 'dist/agent-src/templates/agents/.gitattributes.fragment', size: 2140, why: 'gitattributes fragment the installer appends to a consumer repo' },
|
|
@@ -360,16 +373,25 @@ export interface TypeClassification {
|
|
|
360
373
|
/**
|
|
361
374
|
* Classify the whole payload by type and report what is not accounted for.
|
|
362
375
|
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
376
|
+
* `archive` stays a closed class: no admission path, so an entry in it is
|
|
377
|
+
* always a violation. `binary` is admitted only through `eligible` — the
|
|
378
|
+
* caller supplies the verdicts, this function never decides eligibility itself
|
|
379
|
+
* — and an INELIGIBLE binary is unaccounted exactly as before.
|
|
380
|
+
*
|
|
381
|
+
* The counts are the OBSERVED payload and are never reduced by eligibility. A
|
|
382
|
+
* caller compares `observed` against `allowed` (the number of entries that
|
|
383
|
+
* passed the predicate); nothing here subtracts an excused file from a count,
|
|
384
|
+
* because a measurement an exception can move is not a measurement.
|
|
385
|
+
*
|
|
386
|
+
* Dotfile and extensionless entries are each bound to a path AND a size, so
|
|
387
|
+
* replacing one with different content re-fires the check on that entry rather
|
|
388
|
+
* than passing inside a count.
|
|
368
389
|
*/
|
|
369
390
|
export function classifyPayloadTypes(
|
|
370
391
|
files: readonly PackFile[],
|
|
371
392
|
root = REPO_ROOT,
|
|
372
393
|
head: (p: string) => Uint8Array | null = (p) => readHead(p, root),
|
|
394
|
+
eligible: ReadonlySet<string> = new Set<string>(),
|
|
373
395
|
): TypeClassification {
|
|
374
396
|
const counts: Record<PayloadType | 'unreadable', number> = {
|
|
375
397
|
archive: 0,
|
|
@@ -387,7 +409,11 @@ export function classifyPayloadTypes(
|
|
|
387
409
|
counts[type] += 1;
|
|
388
410
|
if (type === 'text') continue;
|
|
389
411
|
const fingerprint = payloadFingerprint(f.path, f.size);
|
|
390
|
-
|
|
412
|
+
// A binary is accounted for ONLY by the mechanical predicate. It is
|
|
413
|
+
// never matched against the prose table, so a `binary` row added there
|
|
414
|
+
// would still do nothing.
|
|
415
|
+
if (type === 'binary' && eligible.has(f.path)) continue;
|
|
416
|
+
if (type !== 'binary' && type !== 'archive' && byFingerprint.has(fingerprint)) {
|
|
391
417
|
used.add(fingerprint);
|
|
392
418
|
continue;
|
|
393
419
|
}
|
|
@@ -400,6 +426,127 @@ export function classifyPayloadTypes(
|
|
|
400
426
|
};
|
|
401
427
|
}
|
|
402
428
|
|
|
429
|
+
/**
|
|
430
|
+
* Binary eligibility over the packed payload.
|
|
431
|
+
*
|
|
432
|
+
* The probe is a CHILD PROCESS running the engine's own loader
|
|
433
|
+
* (`code_graph/grammar_probe.ts`), not a reimplementation here: a second
|
|
434
|
+
* loader inside the gate could accept bytes the engine rejects, which is the
|
|
435
|
+
* one failure mode that would let a broken grammar ship while the gate called
|
|
436
|
+
* it valid.
|
|
437
|
+
*
|
|
438
|
+
* A probe that cannot run at all yields `null` for every file, which FAILS
|
|
439
|
+
* conditions 8 and 9 rather than passing them — an unverifiable binary is not
|
|
440
|
+
* an eligible one.
|
|
441
|
+
*/
|
|
442
|
+
export interface BinaryEligibilityReport {
|
|
443
|
+
/** Paths that passed every condition. */
|
|
444
|
+
readonly eligible: ReadonlySet<string>;
|
|
445
|
+
/** Every packed binary, eligible or not, with its failing conditions. */
|
|
446
|
+
readonly verdicts: readonly EligibilityVerdict[];
|
|
447
|
+
/** True count of binaries in the payload — never reduced by eligibility. */
|
|
448
|
+
readonly observedCount: number;
|
|
449
|
+
/** True byte total of those binaries. */
|
|
450
|
+
readonly observedBytes: number;
|
|
451
|
+
/** Entries that passed the predicate. Compliance is observed <= allowed. */
|
|
452
|
+
readonly allowedCount: number;
|
|
453
|
+
/** Byte total of the allowed entries. */
|
|
454
|
+
readonly allowedBytes: number;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function evaluateBinaryEligibility(
|
|
458
|
+
files: readonly PackFile[],
|
|
459
|
+
root = REPO_ROOT,
|
|
460
|
+
probeAll: (paths: readonly string[]) => Map<string, GrammarProbe | null> = probeGrammars,
|
|
461
|
+
): BinaryEligibilityReport {
|
|
462
|
+
const manifest = loadManifest(root);
|
|
463
|
+
const packedPaths = new Set(files.map((f) => f.path));
|
|
464
|
+
const binaries = files.filter((f) => classifyEntry(f.path, readHead(f.path, root)) === 'binary');
|
|
465
|
+
const probes = probeAll(binaries.map((b) => b.path));
|
|
466
|
+
|
|
467
|
+
const verdicts: EligibilityVerdict[] = [];
|
|
468
|
+
for (const b of binaries) {
|
|
469
|
+
let bytes: Uint8Array | null = null;
|
|
470
|
+
try {
|
|
471
|
+
bytes = fs.readFileSync(path.join(root, b.path));
|
|
472
|
+
} catch {
|
|
473
|
+
bytes = null;
|
|
474
|
+
}
|
|
475
|
+
verdicts.push(
|
|
476
|
+
evaluateEntry({
|
|
477
|
+
filePath: b.path,
|
|
478
|
+
packedPaths,
|
|
479
|
+
manifest,
|
|
480
|
+
bytes,
|
|
481
|
+
probe: probes.get(b.path) ?? null,
|
|
482
|
+
}),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
const eligible = new Set(verdicts.filter((v) => v.eligible).map((v) => v.path));
|
|
486
|
+
const sizeOf = new Map(files.map((f) => [f.path, f.size]));
|
|
487
|
+
return {
|
|
488
|
+
eligible,
|
|
489
|
+
verdicts,
|
|
490
|
+
observedCount: binaries.length,
|
|
491
|
+
observedBytes: binaries.reduce((n, b) => n + b.size, 0),
|
|
492
|
+
allowedCount: eligible.size,
|
|
493
|
+
allowedBytes: [...eligible].reduce((n, p) => n + (sizeOf.get(p) ?? 0), 0),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Run the production loader over `paths` in one child process. */
|
|
498
|
+
function probeGrammars(paths: readonly string[]): Map<string, GrammarProbe | null> {
|
|
499
|
+
const out = new Map<string, GrammarProbe | null>();
|
|
500
|
+
for (const p of paths) out.set(p, null);
|
|
501
|
+
if (paths.length === 0) return out;
|
|
502
|
+
const script = path.join(REPO_ROOT, 'src/scripts/code_graph/grammar_probe.ts');
|
|
503
|
+
const res = spawnSync('npx', ['tsx', script, ...paths.map((p) => path.join(REPO_ROOT, p))], {
|
|
504
|
+
cwd: REPO_ROOT,
|
|
505
|
+
encoding: 'utf-8',
|
|
506
|
+
});
|
|
507
|
+
if (res.status !== 0 || !res.stdout.trim()) return out;
|
|
508
|
+
try {
|
|
509
|
+
const rows = JSON.parse(res.stdout.trim()) as Array<{
|
|
510
|
+
path: string;
|
|
511
|
+
ok: boolean;
|
|
512
|
+
abi?: number;
|
|
513
|
+
grammar_id?: string;
|
|
514
|
+
}>;
|
|
515
|
+
for (const r of rows) {
|
|
516
|
+
const rel = path.relative(REPO_ROOT, r.path);
|
|
517
|
+
if (r.ok && typeof r.abi === 'number' && typeof r.grammar_id === 'string') {
|
|
518
|
+
out.set(rel, { abi: r.abi, grammar_id: r.grammar_id });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} catch {
|
|
522
|
+
/* an unparseable probe leaves every entry null, which fails 8 and 9 */
|
|
523
|
+
}
|
|
524
|
+
return out;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** The always-printed binary line: the measurement, then the allowance. */
|
|
528
|
+
export function binaryLine(r: BinaryEligibilityReport): string {
|
|
529
|
+
return (
|
|
530
|
+
`binary: observed ${String(r.observedCount)} (${r.observedBytes.toLocaleString('en-US')} B) · ` +
|
|
531
|
+
`allowed ${String(r.allowedCount)} (${r.allowedBytes.toLocaleString('en-US')} B)`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Compliance is `observed <= allowed`.
|
|
537
|
+
*
|
|
538
|
+
* Stated as its own check rather than inferred from the unaccounted list,
|
|
539
|
+
* because the two can only ever agree by construction and a gate that relies on
|
|
540
|
+
* that is one refactor away from silently accepting an excess.
|
|
541
|
+
*/
|
|
542
|
+
export function binaryComplianceViolations(r: BinaryEligibilityReport): string[] {
|
|
543
|
+
if (r.observedCount <= r.allowedCount) return [];
|
|
544
|
+
return [
|
|
545
|
+
`binary payload: observed ${String(r.observedCount)} entries but only ${String(r.allowedCount)} are ` +
|
|
546
|
+
'allowed by src/config/packed-binary-manifest.json under the eligibility predicate',
|
|
547
|
+
];
|
|
548
|
+
}
|
|
549
|
+
|
|
403
550
|
/** One line naming every type class and its count. */
|
|
404
551
|
export function payloadTypeLine(c: TypeClassification): string {
|
|
405
552
|
const parts = [...PAYLOAD_TYPES, 'unreadable' as const]
|
|
@@ -413,11 +560,17 @@ export function typeClassViolations(c: TypeClassification): string[] {
|
|
|
413
560
|
return c.unbound.map(
|
|
414
561
|
(u) =>
|
|
415
562
|
`payload type \`${u.type}\`: ${u.path} (${String(u.size)} bytes) is not accounted for. ` +
|
|
416
|
-
(u.type === 'archive'
|
|
417
|
-
? '
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
563
|
+
(u.type === 'archive'
|
|
564
|
+
? 'Archive is a closed class with no admission path of any kind. ' +
|
|
565
|
+
'Exclude it from package.json files[].'
|
|
566
|
+
: u.type === 'binary'
|
|
567
|
+
? 'A binary ships only via src/config/packed-binary-manifest.json, and only when every ' +
|
|
568
|
+
'condition of the eligibility predicate holds on the packed bytes ' +
|
|
569
|
+
'(_lib/packed_binary_predicate.ts). Add or correct its manifest entry, or exclude it ' +
|
|
570
|
+
'from package.json files[]. The failing conditions are listed above.'
|
|
571
|
+
: 'Every dotfile and extensionless entry is carried by a path-and-size-bound exception. ' +
|
|
572
|
+
`If it belongs in the published surface, add a BoundPayloadException with ` +
|
|
573
|
+
`fingerprint sha256:${u.fingerprint} and a reason; otherwise exclude it from package.json files[].`),
|
|
421
574
|
);
|
|
422
575
|
}
|
|
423
576
|
|
|
@@ -664,6 +817,81 @@ function selfTest(): number {
|
|
|
664
817
|
? 1
|
|
665
818
|
: 0,
|
|
666
819
|
},
|
|
820
|
+
{
|
|
821
|
+
// THE PROSE-PATH SENSITIVITY PROOF. `LICENSE` at 1064 bytes
|
|
822
|
+
// carries a real bound exception, so under the pre-2026-09-07
|
|
823
|
+
// code this entry passed. It is fed binary head bytes here, and
|
|
824
|
+
// binary outranks no-extension in `classifyEntry`, so a
|
|
825
|
+
// prose-exceptable `binary` class would accept it. It must not.
|
|
826
|
+
//
|
|
827
|
+
// STILL LOAD-BEARING after the 2026-09-08 reopening, and more
|
|
828
|
+
// so: `binary` is admissible again, but ONLY through the
|
|
829
|
+
// mechanically verified manifest. This row is what goes red if
|
|
830
|
+
// anyone ever wires the prose table back into the class.
|
|
831
|
+
name: 'type/binary: a manifest-eligible binary IS admitted',
|
|
832
|
+
expect: 'accept',
|
|
833
|
+
// THE OPEN-PATH ACCEPTING CONTROL. The council's own argument
|
|
834
|
+
// against the closed class was that a control nobody saw fire
|
|
835
|
+
// is a claim; the same holds in reverse for a path nobody saw
|
|
836
|
+
// accept. Without this row every binary assertion here is a
|
|
837
|
+
// rejection, and a predicate that only ever refuses is
|
|
838
|
+
// indistinguishable from the hard zero it replaced.
|
|
839
|
+
run: () =>
|
|
840
|
+
typeClassViolations(
|
|
841
|
+
classifyPayloadTypes(
|
|
842
|
+
[{ path: 'src/vendor/grammars/tree-sitter-php.wasm', size: 812594 }],
|
|
843
|
+
REPO_ROOT,
|
|
844
|
+
() => Uint8Array.from([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]),
|
|
845
|
+
new Set(['src/vendor/grammars/tree-sitter-php.wasm']),
|
|
846
|
+
),
|
|
847
|
+
).length > 0
|
|
848
|
+
? 1
|
|
849
|
+
: 0,
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
name: 'type/binary: the SAME path is refused when eligibility did not pass',
|
|
853
|
+
expect: 'reject',
|
|
854
|
+
// The paired negative. Same path, same bytes, empty eligible
|
|
855
|
+
// set — so the admission demonstrably comes from the predicate
|
|
856
|
+
// and not from the path looking familiar.
|
|
857
|
+
run: () =>
|
|
858
|
+
typeClassViolations(
|
|
859
|
+
classifyPayloadTypes(
|
|
860
|
+
[{ path: 'src/vendor/grammars/tree-sitter-php.wasm', size: 812594 }],
|
|
861
|
+
REPO_ROOT,
|
|
862
|
+
() => Uint8Array.from([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]),
|
|
863
|
+
new Set<string>(),
|
|
864
|
+
),
|
|
865
|
+
).length > 0
|
|
866
|
+
? 1
|
|
867
|
+
: 0,
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
name: 'type/binary: an entry with a VALID bound exception is still refused once it reads binary',
|
|
871
|
+
expect: 'reject',
|
|
872
|
+
run: () =>
|
|
873
|
+
typeClassViolations(
|
|
874
|
+
classifyPayloadTypes([{ path: 'LICENSE', size: 1064 }], REPO_ROOT, () =>
|
|
875
|
+
Uint8Array.from([0x7f, 0x45, 0x4c, 0x46, 0x02, 0x00, 0x00]),
|
|
876
|
+
),
|
|
877
|
+
).length > 0
|
|
878
|
+
? 1
|
|
879
|
+
: 0,
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
// The negative control for the row above: the SAME exceptable
|
|
883
|
+
// path with ordinary text still passes on its exception.
|
|
884
|
+
name: 'type/extensionless: the same bound entry with text content still passes',
|
|
885
|
+
expect: 'accept',
|
|
886
|
+
run: () =>
|
|
887
|
+
typeClassViolations(
|
|
888
|
+
classifyPayloadTypes([{ path: 'LICENSE', size: 1064 }], REPO_ROOT, () =>
|
|
889
|
+
new TextEncoder().encode('Apache License\n'),
|
|
890
|
+
),
|
|
891
|
+
).length > 0
|
|
892
|
+
? 1
|
|
893
|
+
: 0,
|
|
894
|
+
},
|
|
667
895
|
{
|
|
668
896
|
name: 'type/extensionless: an unbound extensionless entry is refused',
|
|
669
897
|
expect: 'reject',
|
|
@@ -781,11 +1009,15 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
781
1009
|
// still knows the population it was red over.
|
|
782
1010
|
process.stdout.write(`scanned: ${String(pack.files.length)}\n`);
|
|
783
1011
|
|
|
784
|
-
|
|
1012
|
+
// Binary eligibility, decided on the PACKED bytes before classification so
|
|
1013
|
+
// the classifier is told which paths passed and never decides itself.
|
|
1014
|
+
const binaryReport = evaluateBinaryEligibility(pack.files);
|
|
1015
|
+
const types = classifyPayloadTypes(pack.files, REPO_ROOT, (p) => readHead(p, REPO_ROOT), binaryReport.eligible);
|
|
785
1016
|
const errors = [
|
|
786
1017
|
...evaluate(budget, pack),
|
|
787
1018
|
...classifyPayload(pack.files),
|
|
788
1019
|
...typeClassViolations(types),
|
|
1020
|
+
...binaryComplianceViolations(binaryReport),
|
|
789
1021
|
];
|
|
790
1022
|
const { perSkill, total } = skillBytes(pack.files);
|
|
791
1023
|
if (argv.includes('--json')) {
|
|
@@ -798,6 +1030,13 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
798
1030
|
// what a reader needs to judge a violation: "one unaccounted binary" reads
|
|
799
1031
|
// very differently against a payload of 3 binaries than against one of 0.
|
|
800
1032
|
process.stdout.write(` payload types: ${payloadTypeLine(types)}\n`);
|
|
1033
|
+
// The observed binary count and byte total on EVERY run, green or red.
|
|
1034
|
+
// Eligibility must never move a measurement, so both totals are printed
|
|
1035
|
+
// beside the allowance rather than folded into it.
|
|
1036
|
+
process.stdout.write(` ${binaryLine(binaryReport)}\n`);
|
|
1037
|
+
for (const v of binaryReport.verdicts.filter((x) => !x.eligible)) {
|
|
1038
|
+
for (const f of v.failures) process.stdout.write(` ${v.path}: ${f}\n`);
|
|
1039
|
+
}
|
|
801
1040
|
for (const unused of types.unusedExceptions) {
|
|
802
1041
|
// Not an error: an unbuilt tree legitimately packs fewer files. Named
|
|
803
1042
|
// so a stale exception is visible rather than accumulating unread.
|
|
@@ -19,6 +19,16 @@
|
|
|
19
19
|
* `skipped/`, template/README/open-questions) MUST:
|
|
20
20
|
* 1. Be parseable by the dashboard's `PHASE_RE` (>= one `## Phase <id>`).
|
|
21
21
|
* 2. Have at least one trackable checkbox under every parsed phase.
|
|
22
|
+
* 3. Not declare a RETIRED `status:` value.
|
|
23
|
+
*
|
|
24
|
+
* `status: draft` is the one exemption. It used to have a second, `carrier`,
|
|
25
|
+
* which ADR-262 deleted: a `status:` value never again decides whether an
|
|
26
|
+
* autonomous run may act on a roadmap, so a receiver of deferred work is now
|
|
27
|
+
* ordinary estate and passes normal trackability. A surviving `status: carrier`
|
|
28
|
+
* is REJECTED here with the migration diagnostic rather than silently ignored —
|
|
29
|
+
* deleting a vocabulary term quietly would leave old files wearing a value
|
|
30
|
+
* nothing reads, and the next author would infer a meaning from its presence.
|
|
31
|
+
* The retired set and its diagnostic live in `_lib/retired_status.ts`.
|
|
22
32
|
*
|
|
23
33
|
* Exit codes:
|
|
24
34
|
* 0 — every active roadmap has parseable phases with >= 1 checkbox per phase.
|
|
@@ -31,6 +41,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
31
41
|
|
|
32
42
|
import { checkRatchet } from './_lib/gate_baseline.js';
|
|
33
43
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
44
|
+
import { declaresRetiredStatus, retiredStatusDiagnostic } from './_lib/retired_status.js';
|
|
34
45
|
|
|
35
46
|
const QUIET = process.argv.slice(2).includes('--quiet');
|
|
36
47
|
|
|
@@ -49,15 +60,6 @@ const PHASE_RE =
|
|
|
49
60
|
// FRONTMATTER_RE = re.compile(r"\A---\n(.*?)\n---\s*\n", re.DOTALL)
|
|
50
61
|
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\s*\n/;
|
|
51
62
|
const DRAFT_VALUES: ReadonlySet<string> = new Set(['draft']);
|
|
52
|
-
/**
|
|
53
|
-
* A carrier holds obligations deferred out of an archived parent. It is not
|
|
54
|
-
* schedulable work — every item in it has an unmet resumption trigger — and it
|
|
55
|
-
* has no `## Phase` headings by construction, so it is skipped exactly where a
|
|
56
|
-
* draft is. It differs from a draft in what protects it: a draft may be deleted
|
|
57
|
-
* freely, a carrier may not, and `lint_carrier_integrity` reds when one loses
|
|
58
|
-
* its receiver.
|
|
59
|
-
*/
|
|
60
|
-
const CARRIER_VALUES: ReadonlySet<string> = new Set(['carrier']);
|
|
61
63
|
|
|
62
64
|
const EXCLUDE_NAMES: ReadonlySet<string> = new Set([
|
|
63
65
|
'template.md',
|
|
@@ -101,11 +103,6 @@ function is_draft(fm: Map<string, string>): boolean {
|
|
|
101
103
|
return DRAFT_VALUES.has((fm.get('status') ?? '').toLowerCase());
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
/** True for a `status: carrier` roadmap — see CARRIER_VALUES. */
|
|
105
|
-
function is_carrier(fm: Map<string, string>): boolean {
|
|
106
|
-
return CARRIER_VALUES.has((fm.get('status') ?? '').toLowerCase());
|
|
107
|
-
}
|
|
108
|
-
|
|
109
106
|
/** Mirror update_roadmap_progress.is_roadmap_candidate (POSIX path parts). */
|
|
110
107
|
function is_roadmap_candidate(p: string): boolean {
|
|
111
108
|
const name = path.basename(p);
|
|
@@ -182,7 +179,7 @@ function find_active_roadmaps(root: string): string[] {
|
|
|
182
179
|
continue;
|
|
183
180
|
}
|
|
184
181
|
const text = fs.readFileSync(p, 'utf-8');
|
|
185
|
-
if (is_draft(parse_frontmatter(text))
|
|
182
|
+
if (is_draft(parse_frontmatter(text))) {
|
|
186
183
|
continue;
|
|
187
184
|
}
|
|
188
185
|
out.push(p);
|
|
@@ -193,6 +190,13 @@ function find_active_roadmaps(root: string): string[] {
|
|
|
193
190
|
/** Return human-readable violation strings for a single roadmap. */
|
|
194
191
|
function violations_for(p: string): string[] {
|
|
195
192
|
const text = fs.readFileSync(p, 'utf-8');
|
|
193
|
+
const retired = declaresRetiredStatus(text);
|
|
194
|
+
if (retired !== null) {
|
|
195
|
+
// Returned alone: a file wearing a deleted status has a migration to do
|
|
196
|
+
// before its phase structure is worth reporting on, and stacking phase
|
|
197
|
+
// findings on top would bury the one finding that has to be read first.
|
|
198
|
+
return [`${p}: ${retiredStatusDiagnostic(retired)}`];
|
|
199
|
+
}
|
|
196
200
|
PHASE_RE.lastIndex = 0;
|
|
197
201
|
const matches: Array<{ start: number; end: number; id: string; name: string | undefined }> =
|
|
198
202
|
[];
|
|
@@ -359,7 +363,6 @@ export {
|
|
|
359
363
|
CHECKBOX_RE,
|
|
360
364
|
PHASE_RE,
|
|
361
365
|
is_draft,
|
|
362
|
-
is_carrier,
|
|
363
366
|
is_roadmap_candidate,
|
|
364
367
|
parse_frontmatter,
|
|
365
368
|
find_active_roadmaps,
|