@event4u/agent-config 14.6.0 → 14.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +255 -145
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +264 -10
- package/dist/agent-src/commands/analyze.md +1 -1
- package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
- package/dist/agent-src/commands/evals/roadmap.json +17 -1
- package/dist/agent-src/commands/pr/create.md +14 -2
- package/dist/agent-src/commands/pr/merge.md +379 -0
- package/dist/agent-src/commands/roadmap/process-full.md +272 -32
- package/dist/agent-src/commands/roadmap.md +1 -1
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +95 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +289 -106
- package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
- package/dist/agent-src/rules/context-hygiene.md +2 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +67 -32
- package/dist/agent-src/rules/fix-what-you-see.md +108 -0
- package/dist/agent-src/rules/no-roadmap-references.md +0 -5
- package/dist/agent-src/rules/recurring-criticism.md +110 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +20 -1
- package/dist/agent-src/rules/rule-type-governance.md +0 -2
- package/dist/agent-src/rules/self-repair-loop.md +17 -0
- package/dist/agent-src/rules/skill-quality.md +0 -2
- package/dist/agent-src/rules/source-confidentiality.md +37 -7
- package/dist/agent-src/scripts/archival_sweep.ts +117 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
- package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
- package/dist/agent-src/scripts/gate_budget.ts +282 -0
- package/dist/agent-src/scripts/gate_execute.ts +72 -31
- package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
- package/dist/agent-src/scripts/update_roadmap_progress.ts +296 -53
- package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
- package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
- package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
- package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
- package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
- package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
- package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
- package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
- package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
- package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
- package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
- package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
- package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
- package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
- package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
- package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
- package/dist/agent-src/skills/security/SKILL.md +8 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
- package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
- package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
- package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +65 -4
- package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/dist/cli/registry.js +2 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
- package/dist/cli-delegate/{chunk-P6LQDIWK.js → chunk-7PRE6ZV2.js} +338 -0
- package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
- package/dist/cli-delegate/chunk-E4XIZDIB.js +402 -0
- package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-EEJUMCDZ.js} +16 -16
- package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
- package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-H4VIVD4C.js} +385 -284
- package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
- package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-LPPWO5CE.js} +12 -8
- package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
- package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-N7CDDGI3.js} +4 -23
- package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-PMEKGDED.js} +74 -167
- package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
- package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
- package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
- package/dist/cli-delegate/cmd_conformance.js +466 -716
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +10 -10
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +10 -9
- package/dist/cli-delegate/cmd_handoff.js +10 -9
- package/dist/cli-delegate/cmd_memory_get.js +2 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +1 -1
- package/dist/cli-delegate/cmd_preflight.js +5 -5
- package/dist/cli-delegate/cmd_prune.js +11 -10
- package/dist/cli-delegate/cmd_route_audit.js +6 -5
- package/dist/cli-delegate/cmd_route_explain.js +3 -3
- package/dist/cli-delegate/cmd_session_recycle.js +840 -30
- package/dist/cli-delegate/cmd_settings_get.js +3 -3
- package/dist/cli-delegate/cmd_settings_migrate.js +10 -9
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +11 -10
- package/dist/cli-delegate/cmd_uninstall.js +13 -11
- package/dist/cli-delegate/cmd_update.js +4 -4
- package/dist/cli-delegate/cmd_upgrade.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +11 -10
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +111 -37
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +5 -5
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +15 -12
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +11 -8
- package/dist/hooks/dispatch.js +3165 -2192
- package/dist/install/claudePathsPlan.js +187 -0
- package/dist/install/claudePathsPlan.js.map +1 -0
- package/dist/install/globalRuleLayers.js +169 -0
- package/dist/install/globalRuleLayers.js.map +1 -0
- package/dist/install/hostLayerFingerprint.js +136 -0
- package/dist/install/hostLayerFingerprint.js.map +1 -0
- package/dist/install/install.mjs +553 -425
- package/dist/install/partitionEligibility.js +432 -0
- package/dist/install/partitionEligibility.js.map +1 -0
- package/dist/install/ruleLayerPartition.js +157 -0
- package/dist/install/ruleLayerPartition.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1863 -1234
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_src.js +41 -0
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/scripts/_lib/install_layout.js +83 -0
- package/dist/scripts/_lib/install_layout.js.map +1 -0
- package/dist/scripts/_lib/installed_lock.js +444 -0
- package/dist/scripts/_lib/installed_lock.js.map +1 -0
- package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
- package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
- package/dist/scripts/condense.js +63 -251
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +4 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +12 -6
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
- package/docs/guidelines/agent-infra/false-green.md +47 -1
- package/docs/guidelines/agent-infra/memory-access.md +17 -0
- package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archival_sweep.ts +117 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
- package/src/agent-src/scripts/dashboard_mode.ts +212 -0
- package/src/agent-src/scripts/gate_budget.ts +282 -0
- package/src/agent-src/scripts/gate_execute.ts +72 -31
- package/src/agent-src/scripts/roadmap_gates.ts +74 -3
- package/src/agent-src/scripts/update_roadmap_progress.ts +296 -53
- package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/src/config/agent-settings.template.yml +27 -2
- package/src/config/ci-local-parity.yml +39 -0
- package/src/config/estate-count-budget.json +612 -3
- package/src/config/evaluator-budgets.json +3 -3
- package/src/config/gate-coverage.yml +220 -12
- package/src/config/gate-violation-baselines.json +73 -63
- package/src/config/gitignore-block.txt +15 -2
- package/src/config/hook-latency-budget.json +5 -0
- package/src/config/rule-activation-census.json +14 -6
- package/src/config/rule-stub-ceilings.json +34 -7
- package/src/install/claudePathsPlan.ts +196 -0
- package/src/install/globalRuleLayers.ts +197 -0
- package/src/install/hostLayerFingerprint.ts +145 -0
- package/src/install/partitionEligibility.ts +502 -0
- package/src/install/ruleLayerPartition.ts +212 -0
- package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
- package/src/scripts/_dispatch.bash +17 -1
- package/src/scripts/_lib/ac_heading.ts +30 -0
- package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
- package/src/scripts/_lib/adr_frontmatter.ts +459 -0
- package/src/scripts/_lib/agent_src.ts +42 -0
- package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
- package/src/scripts/_lib/envelope_grounding.ts +49 -9
- package/src/scripts/_lib/installed_lock.ts +50 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
- package/src/scripts/_lib/md_section.ts +129 -0
- package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
- package/src/scripts/_lib/scope_guard.sh +26 -3
- package/src/scripts/_lib/self_repair.ts +19 -0
- package/src/scripts/_lib/self_repair_class_b.ts +171 -0
- package/src/scripts/adr/evidence_census.ts +899 -0
- package/src/scripts/adr/regenerate_index.ts +12 -32
- package/src/scripts/adr_cite_check.ts +540 -39
- package/src/scripts/ai_council/cli_help.ts +55 -0
- package/src/scripts/audit_adr_coverage.ts +103 -17
- package/src/scripts/before_complete_hook.ts +153 -28
- package/src/scripts/bench_ab_v2_complexity.ts +27 -7
- package/src/scripts/bench_hook_latency.ts +204 -0
- package/src/scripts/build_archive_index.ts +34 -0
- package/src/scripts/check_adr_frontmatter.ts +574 -39
- package/src/scripts/check_hook_bundle_content.ts +219 -0
- package/src/scripts/check_new_adr_evidence.ts +529 -0
- package/src/scripts/check_references.ts +78 -0
- package/src/scripts/check_release_published.ts +32 -11
- package/src/scripts/check_rule_layer_partition.ts +531 -0
- package/src/scripts/check_single_delivery.ts +194 -17
- package/src/scripts/check_standing_rule_delivery.ts +8 -2
- package/src/scripts/ci_settle.ts +170 -0
- package/src/scripts/condense.ts +79 -259
- package/src/scripts/council_cli.ts +3 -3
- package/src/scripts/dispatch_r2_reviewer.ts +6 -14
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +77 -14
- package/src/scripts/hooks/block_no_verify.ts +61 -10
- package/src/scripts/hooks/block_unauthorized_git.ts +21 -1
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/dispatch_hook.ts +154 -187
- package/src/scripts/hooks/dispatch_issues.ts +38 -19
- package/src/scripts/hooks/fallback_yaml.ts +66 -0
- package/src/scripts/hooks/host_semantics.ts +21 -0
- package/src/scripts/hooks/injection_budget.ts +13 -3
- package/src/scripts/hooks/py_json_dumps.ts +103 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
- package/src/scripts/hooks/run_continuation_hook.ts +8 -5
- package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
- package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
- package/src/scripts/hooks/state_io.ts +870 -21
- package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
- package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
- package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
- package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
- package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
- package/src/scripts/injection_scan_hook.ts +137 -5
- package/src/scripts/install.ts +16 -11
- package/src/scripts/language_mirror_hook.ts +561 -25
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_hook_manifest.ts +49 -0
- package/src/scripts/lint_plan_risk_register.ts +4 -3
- package/src/scripts/lint_provenance_vocabulary.ts +472 -4
- package/src/scripts/lint_skill_router_head.ts +4 -4
- package/src/scripts/mcp_server/tools.ts +2 -2
- package/src/scripts/new_roadmap.ts +136 -0
- package/src/scripts/pack_mcp_content.ts +20 -5
- package/src/scripts/release.ts +37 -38
- package/src/scripts/report_layer_overlap.ts +6 -0
- package/src/scripts/routing_doctor.ts +5 -1
- package/src/scripts/self_repair_hook.ts +37 -5
- package/src/scripts/session_register_hook.ts +11 -0
- package/src/scripts/skill_usage_collect.ts +11 -1
- package/src/scripts/skill_usage_report.ts +278 -8
- package/src/scripts/sync_pr_branch.ts +170 -20
- package/src/server/schemas/settings.ts +8 -0
- package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
- package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
- package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
- package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
- package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-MMSGVZKV.js} +3 -3
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* A NEW accepted ADR must disclose its evidence — the half of AC-5 that had no
|
|
4
|
+
* mechanism.
|
|
5
|
+
*
|
|
6
|
+
* AC-5 of `road-to-evidence-based-adr-governance` claims that "a newly added
|
|
7
|
+
* accepted ADR cannot pass CI without an Evidence section, a substantive
|
|
8
|
+
* `review_trigger`, and no unscoped permanence language". Two of those three
|
|
9
|
+
* were enforced by `check_adr_frontmatter`. The first was not, and that gate's
|
|
10
|
+
* own docstring said so: `check_descriptive_axes` validates the SHAPE of an axis
|
|
11
|
+
* that is PRESENT, so a fresh record with no `provenance`, no `evidence` and no
|
|
12
|
+
* `## Evidence` section passed every gate in this tree.
|
|
13
|
+
*
|
|
14
|
+
* The blocker was real and structural rather than an omission: requiring the
|
|
15
|
+
* axes needs a notion of NEW, and a single-file linter has none — it sees a
|
|
16
|
+
* `date:` the author typed, not whether the record is new to the repository, and
|
|
17
|
+
* a backdated record slips straight through that proxy. The fix named there was
|
|
18
|
+
* "a two-ref diff, which is buildable and not built". This is that diff.
|
|
19
|
+
*
|
|
20
|
+
* ## What it requires, and of what
|
|
21
|
+
*
|
|
22
|
+
* Scope: records **ADDED** relative to a base ref (`git diff --diff-filter=A`),
|
|
23
|
+
* under `docs/decisions/ADR-*.md` and `docs/adrs/<area>/NNNN-*.md`. Records that
|
|
24
|
+
* already existed are untouched — the 147 accepted records in the corpus predate
|
|
25
|
+
* the axes, and retrofitting them is a migration event this gate deliberately is
|
|
26
|
+
* not. Same scope decision, same reasoning, as `lint_evidence_artifacts`.
|
|
27
|
+
*
|
|
28
|
+
* Of an added record whose `status:` is `accepted`, all three:
|
|
29
|
+
*
|
|
30
|
+
* 1. a `provenance:` map carrying a `kind:`
|
|
31
|
+
* 2. an `evidence:` map carrying a `strength:`
|
|
32
|
+
* 3. an `## Evidence` section in the body (heading at any level)
|
|
33
|
+
*
|
|
34
|
+
* `authority_basis: owner_intent` does NOT excuse the section. That field
|
|
35
|
+
* governs where a record's AUTHORITY comes from — an owner purpose statement is
|
|
36
|
+
* legitimately E0 and its alternatives are foreclosed by ownership — and it says
|
|
37
|
+
* nothing about whether the record discloses what it rests on. Reading it as a
|
|
38
|
+
* disclosure exemption would turn the one honest escape into the hole.
|
|
39
|
+
*
|
|
40
|
+
* Non-`accepted` added records (`proposed`, `superseded`, …) are OUT OF SCOPE
|
|
41
|
+
* and are reported as such, per file, rather than silently skipped: a record can
|
|
42
|
+
* legitimately ship `proposed` precisely so its acceptance is separately
|
|
43
|
+
* reviewable (ADR-239 does), and a gate that prints nothing about them is
|
|
44
|
+
* indistinguishable from a gate that failed to see them.
|
|
45
|
+
*
|
|
46
|
+
* ## What it does NOT do
|
|
47
|
+
*
|
|
48
|
+
* It checks the axes are PRESENT, never that their values are valid. Vocabulary
|
|
49
|
+
* validation (`kind` ∈ {human, agentic, mixed, unknown}, `strength` ∈ E0…E4,
|
|
50
|
+
* the `review_trigger` staging, permanence language) is
|
|
51
|
+
* `check_adr_frontmatter`'s and stays there — one axis, one owner. Nor does it
|
|
52
|
+
* judge whether the `## Evidence` section says anything true; no gate can, and
|
|
53
|
+
* that stays a human read at review time.
|
|
54
|
+
*
|
|
55
|
+
* It reads frontmatter through the SHARED reader `_lib/adr_frontmatter.ts`. The
|
|
56
|
+
* tree carried three divergent ADR parsers until that extraction; a fourth
|
|
57
|
+
* would have re-created the exact divergence — and the nested axes are the
|
|
58
|
+
* shapes the old regex reader silently read as absent.
|
|
59
|
+
*
|
|
60
|
+
* Usage:
|
|
61
|
+
* ./scripts-run src/scripts/check_new_adr_evidence # gate
|
|
62
|
+
* ./scripts-run src/scripts/check_new_adr_evidence --base REF
|
|
63
|
+
* ./scripts-run src/scripts/check_new_adr_evidence --self-test
|
|
64
|
+
*
|
|
65
|
+
* Exit codes: 0 clean (including "nothing added") · 1 violation · 2 usage/env.
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
import { execFileSync } from 'node:child_process';
|
|
69
|
+
import * as fs from 'node:fs';
|
|
70
|
+
import * as os from 'node:os';
|
|
71
|
+
import * as path from 'node:path';
|
|
72
|
+
import process from 'node:process';
|
|
73
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
74
|
+
|
|
75
|
+
import { evidenceOf, provenanceOf, readAdrFrontmatter } from './_lib/adr_frontmatter.js';
|
|
76
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
77
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
78
|
+
import { resolveBaseRef } from './_lib/ratchet_base_ref.js';
|
|
79
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
80
|
+
|
|
81
|
+
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
82
|
+
const REPO_ROOT = path.resolve(_HERE, '..', '..');
|
|
83
|
+
|
|
84
|
+
/** The two decision surfaces that carry numbered records. */
|
|
85
|
+
export const ADR_PATHSPECS = [
|
|
86
|
+
'docs/decisions/ADR-*.md',
|
|
87
|
+
'docs/adrs/*/[0-9][0-9][0-9][0-9]-*.md',
|
|
88
|
+
] as const;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The same two surfaces as anchored regexes.
|
|
92
|
+
*
|
|
93
|
+
* Git's default pathspec wildcards match `/`, so `docs/adrs/*/NNNN-*.md` can
|
|
94
|
+
* over-match a deeper path. The pathspecs narrow what git has to walk; these
|
|
95
|
+
* decide what is in scope. Belt and braces, because an over-match here is a
|
|
96
|
+
* false red on somebody else's file.
|
|
97
|
+
*/
|
|
98
|
+
const ADR_PATH_RES = [
|
|
99
|
+
/^docs\/decisions\/ADR-[^/]*\.md$/,
|
|
100
|
+
/^docs\/adrs\/[^/]+\/\d{4}-[^/]*\.md$/,
|
|
101
|
+
] as const;
|
|
102
|
+
|
|
103
|
+
/** Fenced blocks, so a quoted `## Evidence` inside one is not a declaration. */
|
|
104
|
+
const FENCE_RE = /^(?:```|~~~)/;
|
|
105
|
+
|
|
106
|
+
/** An `Evidence` heading at any level. The heading, never a mention of the word. */
|
|
107
|
+
const EVIDENCE_HEADING_RE = /^#{1,6}[ \t]+Evidence\b/;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Does the body carry an `## Evidence` section?
|
|
111
|
+
*
|
|
112
|
+
* Fenced blocks are stripped first. An ADR that quotes a template — and several
|
|
113
|
+
* in this corpus do — would otherwise satisfy the check by showing what the
|
|
114
|
+
* section looks like instead of by having one.
|
|
115
|
+
*
|
|
116
|
+
* A heading is accepted when its text STARTS with `Evidence`, so
|
|
117
|
+
* `## Evidence and assumptions` counts. The alternative (exact match) would
|
|
118
|
+
* reject a legitimate compound heading, and the failure mode of this check is
|
|
119
|
+
* the one worth minimising: it is a disclosure floor, not a style rule.
|
|
120
|
+
*/
|
|
121
|
+
export function hasEvidenceSection(text: string): boolean {
|
|
122
|
+
let inFence = false;
|
|
123
|
+
for (const raw of text.split('\n')) {
|
|
124
|
+
const line = raw.trim();
|
|
125
|
+
if (FENCE_RE.test(line)) {
|
|
126
|
+
inFence = !inFence;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (inFence) continue;
|
|
130
|
+
if (EVIDENCE_HEADING_RE.test(line)) return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function _git(root: string, args: readonly string[]): string {
|
|
136
|
+
// GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE are inherited inside a git hook
|
|
137
|
+
// and point at the INVOKING repository, so a `cwd`-scoped call silently
|
|
138
|
+
// reads the wrong tree. This repo has already recorded that failure once.
|
|
139
|
+
const env = { ...process.env };
|
|
140
|
+
delete env['GIT_DIR'];
|
|
141
|
+
delete env['GIT_WORK_TREE'];
|
|
142
|
+
delete env['GIT_INDEX_FILE'];
|
|
143
|
+
return execFileSync('git', [...args], {
|
|
144
|
+
cwd: root,
|
|
145
|
+
encoding: 'utf8',
|
|
146
|
+
env,
|
|
147
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Decision records ADDED relative to `baseRef`. */
|
|
152
|
+
export function addedAdrRecords(root: string, baseRef: string): string[] {
|
|
153
|
+
const out = _git(root, [
|
|
154
|
+
'diff',
|
|
155
|
+
'--name-only',
|
|
156
|
+
'--diff-filter=A',
|
|
157
|
+
`${baseRef}...HEAD`,
|
|
158
|
+
'--',
|
|
159
|
+
...ADR_PATHSPECS,
|
|
160
|
+
]);
|
|
161
|
+
return out
|
|
162
|
+
.split('\n')
|
|
163
|
+
.map((s) => s.trim())
|
|
164
|
+
.filter((s) => s !== '' && ADR_PATH_RES.some((re) => re.test(s)))
|
|
165
|
+
.sort();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface Finding {
|
|
169
|
+
readonly file: string;
|
|
170
|
+
readonly reason: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Records considered, split by what the gate did with each. */
|
|
174
|
+
export interface CheckResult {
|
|
175
|
+
readonly findings: Finding[];
|
|
176
|
+
/** Added records whose status is not `accepted` — named, not hidden. */
|
|
177
|
+
readonly outOfScope: { readonly file: string; readonly status: string }[];
|
|
178
|
+
readonly checked: string[];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const REMEDY =
|
|
182
|
+
'add `provenance:` (with `kind:`), `evidence:` (with `strength:`) and an ' +
|
|
183
|
+
'`## Evidence` section — see docs/contracts/adr-layout.md and ADR-239. ' +
|
|
184
|
+
'`authority_basis: owner_intent` governs authority, never disclosure, and ' +
|
|
185
|
+
'does not exempt any of the three.';
|
|
186
|
+
|
|
187
|
+
export function checkRecords(root: string, files: readonly string[], ledger?: GateLedger): CheckResult {
|
|
188
|
+
const findings: Finding[] = [];
|
|
189
|
+
const outOfScope: { file: string; status: string }[] = [];
|
|
190
|
+
const checked: string[] = [];
|
|
191
|
+
ledger?.plan([...files]);
|
|
192
|
+
|
|
193
|
+
for (const rel of files) {
|
|
194
|
+
let text: string;
|
|
195
|
+
try {
|
|
196
|
+
text = fs.readFileSync(path.join(root, rel), 'utf8');
|
|
197
|
+
} catch {
|
|
198
|
+
// Listed by git, absent on disk: an added-then-moved path in a dirty
|
|
199
|
+
// tree hits this and is somebody else's gate, not a finding here.
|
|
200
|
+
ledger?.outOfScope(rel, 'no_applicable_files');
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const fm = readAdrFrontmatter(text);
|
|
205
|
+
if (fm === null) {
|
|
206
|
+
// A NEW record with no YAML block cannot be classified at all — its
|
|
207
|
+
// status is unreadable, so "is it accepted" has no answer. Failing is
|
|
208
|
+
// the honest direction: the alternative is to let an unreadable
|
|
209
|
+
// record through the one gate that exists to read it.
|
|
210
|
+
ledger?.fail(rel, 'no YAML frontmatter');
|
|
211
|
+
findings.push({
|
|
212
|
+
file: rel,
|
|
213
|
+
reason: 'no YAML frontmatter — a new decision record carries one, so its status and axes can be read',
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const status = (fm.scalars['status'] ?? '').trim();
|
|
219
|
+
if (status !== 'accepted') {
|
|
220
|
+
outOfScope.push({ file: rel, status: status === '' ? '(unset)' : status });
|
|
221
|
+
ledger?.outOfScope(rel, 'not_applicable_kind');
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const missing: string[] = [];
|
|
226
|
+
if (provenanceOf(fm)?.kind == null) missing.push('`provenance.kind`');
|
|
227
|
+
if (evidenceOf(fm)?.strength == null) missing.push('`evidence.strength`');
|
|
228
|
+
if (!hasEvidenceSection(text)) missing.push('an `## Evidence` section');
|
|
229
|
+
|
|
230
|
+
if (missing.length > 0) {
|
|
231
|
+
ledger?.fail(rel, `missing ${missing.join(', ')}`);
|
|
232
|
+
findings.push({
|
|
233
|
+
file: rel,
|
|
234
|
+
reason: `accepted on arrival and missing ${missing.join(', ')} — ${REMEDY}`,
|
|
235
|
+
});
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
checked.push(rel);
|
|
239
|
+
ledger?.complete(rel);
|
|
240
|
+
}
|
|
241
|
+
return { findings, outOfScope, checked };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function _argValue(argv: readonly string[], flag: string): string | null {
|
|
245
|
+
const i = argv.indexOf(flag);
|
|
246
|
+
if (i === -1) return null;
|
|
247
|
+
const v = argv[i + 1];
|
|
248
|
+
return v === undefined || v.startsWith('--') ? null : v;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
252
|
+
if (argv.includes('--self-test')) return selfTest();
|
|
253
|
+
|
|
254
|
+
const root = process.cwd();
|
|
255
|
+
|
|
256
|
+
// `resolveBaseRef` rather than a hardcoded `origin/main`: `actions/checkout`
|
|
257
|
+
// performs a shallow PR-merge fetch, so a PR build frequently has no
|
|
258
|
+
// `origin/main` remote-tracking ref at all. That ladder is the repo's one
|
|
259
|
+
// answer to this question and it needs no network on those builds.
|
|
260
|
+
const baseRef = _argValue(argv, '--base') ?? resolveBaseRef(root);
|
|
261
|
+
if (baseRef === null) {
|
|
262
|
+
// Fail rather than compare against an assumed-empty base. An
|
|
263
|
+
// unresolvable base makes every record look un-added, and the gate would
|
|
264
|
+
// then pass by scanning nothing — the precise false green it exists to
|
|
265
|
+
// remove from AC-5.
|
|
266
|
+
process.stderr.write(
|
|
267
|
+
'check_new_adr_evidence: no base ref resolved — pass --base <ref> or set RATCHET_BASE_REF\n',
|
|
268
|
+
);
|
|
269
|
+
return 2;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let files: string[];
|
|
273
|
+
try {
|
|
274
|
+
files = addedAdrRecords(root, baseRef);
|
|
275
|
+
} catch (exc) {
|
|
276
|
+
process.stderr.write(`check_new_adr_evidence: cannot diff against ${baseRef} — ${String(exc)}\n`);
|
|
277
|
+
return 2;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const ledger = new GateLedger('check_new_adr_evidence');
|
|
281
|
+
const result = checkRecords(root, files, ledger);
|
|
282
|
+
ledger.report();
|
|
283
|
+
|
|
284
|
+
// Zero added records is the NORMAL case, so it must exit green — and say so.
|
|
285
|
+
// `assertScanned` throws on a zero count unless a justified `allowEmpty`
|
|
286
|
+
// reason is supplied, and the reason here is `EMPTY_VALID` rather than
|
|
287
|
+
// `OPTIONAL_INPUT`: the scope is a DIFF, not a corpus, so "this branch added
|
|
288
|
+
// no decision record" genuinely is the pass, and stays true even if
|
|
289
|
+
// `docs/decisions/` were deleted outright. The line is still emitted, because
|
|
290
|
+
// a silent green over an empty set is how a gate stops measuring anything
|
|
291
|
+
// without anyone noticing.
|
|
292
|
+
//
|
|
293
|
+
// The honest cost, named rather than implied: an unconditional `allowEmpty`
|
|
294
|
+
// means the scope assertion can never fire for this gate, so it buys no
|
|
295
|
+
// dead-root protection. What replaces it is `--self-test`, which proves the
|
|
296
|
+
// verdict still moves — the property a `scanned:` floor cannot establish for
|
|
297
|
+
// a gate whose usual reading is zero.
|
|
298
|
+
reportScanned({
|
|
299
|
+
gate: 'check_new_adr_evidence',
|
|
300
|
+
scanned: files.length,
|
|
301
|
+
units: 'added decision record(s)',
|
|
302
|
+
roots: ['docs/decisions', 'docs/adrs'],
|
|
303
|
+
allowEmpty:
|
|
304
|
+
'EMPTY_VALID: the scope is the set of records ADDED since the base ref; ' +
|
|
305
|
+
'a branch that adds none has nothing to disclose, and zero is the pass.',
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (files.length === 0) {
|
|
309
|
+
process.stdout.write(
|
|
310
|
+
`check_new_adr_evidence: ✅ no decision record added since ${baseRef} — nothing to check\n`,
|
|
311
|
+
);
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const row of result.outOfScope) {
|
|
316
|
+
process.stdout.write(
|
|
317
|
+
` ⚪ ${row.file}: status ${row.status} — out of scope; this gate requires disclosure of \`accepted\` records only\n`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
process.stdout.write(
|
|
322
|
+
`check_new_adr_evidence: ${String(files.length)} added record(s) since ${baseRef} · ` +
|
|
323
|
+
`${String(result.checked.length)} accepted and disclosing · ` +
|
|
324
|
+
`${String(result.outOfScope.length)} not accepted · ` +
|
|
325
|
+
`${String(result.findings.length)} violation(s)\n`,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
if (result.findings.length > 0) {
|
|
329
|
+
for (const f of result.findings) {
|
|
330
|
+
process.stderr.write(` ❌ ${f.file}: ${f.reason}\n`);
|
|
331
|
+
}
|
|
332
|
+
process.stderr.write(
|
|
333
|
+
`\n❌ check_new_adr_evidence: ${String(result.findings.length)} new record(s) claim ` +
|
|
334
|
+
'accepted status without disclosing what they rest on.\n',
|
|
335
|
+
);
|
|
336
|
+
return 1;
|
|
337
|
+
}
|
|
338
|
+
process.stdout.write('✅ check_new_adr_evidence: no violations\n');
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
// --self-test — drives the REAL binary over synthetic repositories.
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Why this gate needs a self-test more than most.
|
|
348
|
+
*
|
|
349
|
+
* Its normal reading is ZERO added records, and that is exactly the state where
|
|
350
|
+
* a working gate and a completely broken one emit the same output: `scanned: 0`,
|
|
351
|
+
* green. A `min_scanned` floor cannot separate them — a floor of 0 can never
|
|
352
|
+
* trip, and any higher floor would red every branch that adds no ADR. Only
|
|
353
|
+
* feeding it a synthetic added-accepted-record-without-evidence and watching the
|
|
354
|
+
* verdict move proves the reading still changes the answer.
|
|
355
|
+
*
|
|
356
|
+
* Each case builds a throwaway git repository, commits a base, then commits the
|
|
357
|
+
* record on top, and runs the real CLI with `--base <base-sha>` — so the diff
|
|
358
|
+
* path, the argv parsing and the entry guard are all under test rather than an
|
|
359
|
+
* imported function.
|
|
360
|
+
*/
|
|
361
|
+
interface FixtureOpts {
|
|
362
|
+
/** Frontmatter body between the `---` markers. */
|
|
363
|
+
readonly frontmatter: string;
|
|
364
|
+
/** Markdown body after the frontmatter. */
|
|
365
|
+
readonly body: string;
|
|
366
|
+
/** Repo-relative path for the record; defaults to a flat ADR. */
|
|
367
|
+
readonly rel?: string;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function _gitQuiet(root: string, args: readonly string[]): void {
|
|
371
|
+
const env = { ...process.env };
|
|
372
|
+
delete env['GIT_DIR'];
|
|
373
|
+
delete env['GIT_WORK_TREE'];
|
|
374
|
+
delete env['GIT_INDEX_FILE'];
|
|
375
|
+
execFileSync(
|
|
376
|
+
'git',
|
|
377
|
+
[
|
|
378
|
+
'-c',
|
|
379
|
+
'user.email=selftest@example.invalid',
|
|
380
|
+
'-c',
|
|
381
|
+
'user.name=self-test',
|
|
382
|
+
'-c',
|
|
383
|
+
'commit.gpgsign=false',
|
|
384
|
+
'-c',
|
|
385
|
+
'core.hooksPath=',
|
|
386
|
+
...args,
|
|
387
|
+
],
|
|
388
|
+
{ cwd: root, encoding: 'utf8', env, stdio: 'pipe' },
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function _write(root: string, rel: string, contents: string): void {
|
|
393
|
+
const abs = path.join(root, rel);
|
|
394
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
395
|
+
fs.writeFileSync(abs, contents, 'utf8');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** A repo whose HEAD adds `record` (or nothing) on top of a committed base. */
|
|
399
|
+
function _repo(parent: string, record: FixtureOpts | null): { root: string; base: string } {
|
|
400
|
+
const root = fs.mkdtempSync(path.join(parent, 'repo-'));
|
|
401
|
+
_gitQuiet(root, ['init', '-q']);
|
|
402
|
+
_write(root, 'README.md', '# base\n');
|
|
403
|
+
_gitQuiet(root, ['add', '-A']);
|
|
404
|
+
_gitQuiet(root, ['commit', '-q', '-m', 'base']);
|
|
405
|
+
const base = _git(root, ['rev-parse', 'HEAD']).trim();
|
|
406
|
+
if (record !== null) {
|
|
407
|
+
_write(
|
|
408
|
+
root,
|
|
409
|
+
record.rel ?? 'docs/decisions/ADR-901-probe.md',
|
|
410
|
+
`---\n${record.frontmatter}\n---\n\n# ADR-901 — probe\n\n${record.body}\n`,
|
|
411
|
+
);
|
|
412
|
+
_gitQuiet(root, ['add', '-A']);
|
|
413
|
+
_gitQuiet(root, ['commit', '-q', '-m', 'add record']);
|
|
414
|
+
}
|
|
415
|
+
return { root, base };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const FULL_FRONTMATTER = [
|
|
419
|
+
'adr: 901',
|
|
420
|
+
'status: accepted',
|
|
421
|
+
'date: 2026-08-21',
|
|
422
|
+
'decision: probe',
|
|
423
|
+
'review_trigger: Reopen when the probe stops probing.',
|
|
424
|
+
'provenance:',
|
|
425
|
+
' kind: mixed',
|
|
426
|
+
' decision_makers: [owner]',
|
|
427
|
+
'evidence:',
|
|
428
|
+
' strength: E3',
|
|
429
|
+
' discovery: complete',
|
|
430
|
+
].join('\n');
|
|
431
|
+
|
|
432
|
+
export function selfTest(): number {
|
|
433
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cnae-'));
|
|
434
|
+
|
|
435
|
+
const run = (record: FixtureOpts | null): number => {
|
|
436
|
+
const { root, base } = _repo(tmp, record);
|
|
437
|
+
return runGateCli(REPO_ROOT, 'src/scripts/check_new_adr_evidence.ts', ['--base', base], root);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const withoutLines = (drop: RegExp): string =>
|
|
441
|
+
FULL_FRONTMATTER.split('\n')
|
|
442
|
+
.filter((l) => !drop.test(l))
|
|
443
|
+
.join('\n');
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
return runSelfTest({
|
|
447
|
+
gate: 'check_new_adr_evidence',
|
|
448
|
+
minCases: 7,
|
|
449
|
+
minRejectCases: 4,
|
|
450
|
+
cases: [
|
|
451
|
+
{
|
|
452
|
+
name: 'a branch that adds no record passes — the normal case must stay green',
|
|
453
|
+
expect: 'accept',
|
|
454
|
+
run: () => run(null),
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
name: 'an added accepted record with both axes and an `## Evidence` section passes',
|
|
458
|
+
expect: 'accept',
|
|
459
|
+
run: () =>
|
|
460
|
+
run({ frontmatter: FULL_FRONTMATTER, body: '## Evidence\n\nMeasured.\n' }),
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: 'an added accepted record with NO `## Evidence` section is rejected — the AC-5 gap itself',
|
|
464
|
+
expect: 'reject',
|
|
465
|
+
run: () => run({ frontmatter: FULL_FRONTMATTER, body: '## Context\n\nNothing.\n' }),
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: 'an added accepted record with no `evidence.strength` is rejected',
|
|
469
|
+
expect: 'reject',
|
|
470
|
+
run: () =>
|
|
471
|
+
run({
|
|
472
|
+
frontmatter: withoutLines(/^(evidence:|\s+strength:|\s+discovery:)/),
|
|
473
|
+
body: '## Evidence\n\nMeasured.\n',
|
|
474
|
+
}),
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: 'an added accepted record with no `provenance.kind` is rejected',
|
|
478
|
+
expect: 'reject',
|
|
479
|
+
run: () =>
|
|
480
|
+
run({
|
|
481
|
+
frontmatter: withoutLines(/^(provenance:|\s+kind:|\s+decision_makers:)/),
|
|
482
|
+
body: '## Evidence\n\nMeasured.\n',
|
|
483
|
+
}),
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: '`authority_basis: owner_intent` does not excuse the section — authority is not disclosure',
|
|
487
|
+
expect: 'reject',
|
|
488
|
+
run: () =>
|
|
489
|
+
run({
|
|
490
|
+
frontmatter: `${FULL_FRONTMATTER}\nauthority_basis: owner_intent`,
|
|
491
|
+
body: '## Context\n\nOwner says so.\n',
|
|
492
|
+
}),
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: 'a non-accepted added record is out of scope, not a violation',
|
|
496
|
+
expect: 'accept',
|
|
497
|
+
run: () =>
|
|
498
|
+
run({
|
|
499
|
+
frontmatter: FULL_FRONTMATTER.replace('status: accepted', 'status: proposed')
|
|
500
|
+
.split('\n')
|
|
501
|
+
.filter((l) => !/^(provenance:|evidence:|\s{2}\w+:)/.test(l))
|
|
502
|
+
.join('\n'),
|
|
503
|
+
body: '## Context\n\nStill being argued.\n',
|
|
504
|
+
}),
|
|
505
|
+
},
|
|
506
|
+
],
|
|
507
|
+
});
|
|
508
|
+
} finally {
|
|
509
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Main-guard (realpath-compared, mirrors the repo convention).
|
|
514
|
+
function _isCliEntry(): boolean {
|
|
515
|
+
if (process.argv[1] === undefined) return false;
|
|
516
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
517
|
+
if (import.meta.url === argvUrl) return true;
|
|
518
|
+
try {
|
|
519
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
520
|
+
const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
|
|
521
|
+
return here === argv1;
|
|
522
|
+
} catch {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (_isCliEntry()) {
|
|
528
|
+
process.exit(main());
|
|
529
|
+
}
|
|
@@ -97,6 +97,13 @@ const FILE_SKIP_MARKER = '<!-- check-refs: skip -->';
|
|
|
97
97
|
// fully-checked documents.
|
|
98
98
|
const LINE_IGNORE_MARKER = '<!-- ref-ignore -->';
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The one generated artefact this repository intentionally does not track, so
|
|
102
|
+
* no clone carries it and an existence check cannot answer for it. Referencing
|
|
103
|
+
* it in prose is legitimate; the path simply is not a file in CI.
|
|
104
|
+
*/
|
|
105
|
+
const UNTRACKED_GENERATED_DASHBOARD = 'agents/roadmaps-progress.md';
|
|
106
|
+
|
|
100
107
|
// YAML memory files (engineering-memory layer) live under `agents/memory/`.
|
|
101
108
|
// Each entry may reference skills, ADR paths, or local files via
|
|
102
109
|
// `source:` / `enforcement:` / `skill:`. We validate those paths so a
|
|
@@ -473,6 +480,57 @@ function _extract_personas_frontmatter(text: string): Array<[number, string]> {
|
|
|
473
480
|
return results;
|
|
474
481
|
}
|
|
475
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Paths this checker must not call broken because nothing is allowed to fix them.
|
|
485
|
+
*
|
|
486
|
+
* `archive_completed_roadmaps` treats `agents/evidence/**` and every `*.patch` as
|
|
487
|
+
* a FROZEN RECORD and deliberately does not rewrite path strings there — a review
|
|
488
|
+
* artefact is a record of what a reviewer was shown, and editing it retroactively
|
|
489
|
+
* makes it claim something that did not happen (measured 2026-08-11: one archival
|
|
490
|
+
* rewrote four frozen artefacts including a `diff.patch` header, and reported it
|
|
491
|
+
* as "4 ref(s) migrated"). So when a roadmap is archived, a record naming its old
|
|
492
|
+
* path keeps naming it — correctly.
|
|
493
|
+
*
|
|
494
|
+
* Without this carve-out the two gates contradict each other: the sweep forbids
|
|
495
|
+
* the rewrite and this checker reds the un-rewritten path, so EVERY archival of a
|
|
496
|
+
* roadmap a review artefact mentions turns CI red with no compliant fix available.
|
|
497
|
+
* Measured on the change that made archival automatic: 2 such references.
|
|
498
|
+
*
|
|
499
|
+
* Deliberately narrow — three conditions, all required. The source is a frozen
|
|
500
|
+
* record; the reference is a top-level roadmap path; and the file is still
|
|
501
|
+
* findable under a terminal disposition. A frozen record pointing at a roadmap
|
|
502
|
+
* that exists NOWHERE is still broken and still reds, which is the part a blanket
|
|
503
|
+
* `agents/evidence/reviews` exclusion would have lost: that directory holds live
|
|
504
|
+
* `*.findings.md` work alongside archived records, so excluding it wholesale would
|
|
505
|
+
* stop checking documents that are still being acted on.
|
|
506
|
+
*
|
|
507
|
+
* The frozen-record predicate is duplicated from `_is_frozen_record` in
|
|
508
|
+
* `src/agent-src/scripts/archive_completed_roadmaps.ts` rather than imported: that
|
|
509
|
+
* module is projected into consumer installs and pulls the dashboard generator in
|
|
510
|
+
* with it, so a package-internal gate importing it would drag the projection layer
|
|
511
|
+
* into CI's gate path. `tests/scripts/check_references.test.ts` holds the two
|
|
512
|
+
* copies against one table so the duplication cannot drift silently.
|
|
513
|
+
*/
|
|
514
|
+
const _FROZEN_RECORD_PREFIXES = ['agents/evidence/'];
|
|
515
|
+
const _TERMINAL_ROADMAP_DIRS = ['archive', 'skipped', 'later'];
|
|
516
|
+
|
|
517
|
+
export function _is_frozen_record_source(rel: string): boolean {
|
|
518
|
+
const norm = rel.split(path.sep).join('/');
|
|
519
|
+
return _FROZEN_RECORD_PREFIXES.some((p) => norm.startsWith(p)) || norm.endsWith('.patch');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Does `ref` name a top-level roadmap that now lives under a terminal disposition? */
|
|
523
|
+
export function _resolves_under_terminal_disposition(ref: string, root: string): boolean {
|
|
524
|
+
const norm = ref.split(path.sep).join('/');
|
|
525
|
+
const m = /^agents\/roadmaps\/([^/]+\.md)$/.exec(norm);
|
|
526
|
+
if (m === null) {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
return _TERMINAL_ROADMAP_DIRS.some((d) =>
|
|
530
|
+
_exists(path.join(root, 'agents', 'roadmaps', d, m[1] as string)),
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
476
534
|
function _find_suggestion(p: string, root: string): string {
|
|
477
535
|
const name = path.basename(p);
|
|
478
536
|
// DEAD-ROOT REPAIR (road-to-renewal-foundation Phase 1): the middle entry
|
|
@@ -664,6 +722,25 @@ function check_file(
|
|
|
664
722
|
// check above already swallows refs into `agents/runtime/`,
|
|
665
723
|
// so no extra carve-out is needed.
|
|
666
724
|
}
|
|
725
|
+
if (!resolved && rawRef === UNTRACKED_GENERATED_DASHBOARD) {
|
|
726
|
+
// A generated LOCAL artefact this repository deliberately does
|
|
727
|
+
// not commit (2026-08-21: it was the #1 merge-conflict path).
|
|
728
|
+
// It exists on a developer's disk and in no clone, so an
|
|
729
|
+
// existence check is the wrong question for it — same shape as
|
|
730
|
+
// the gitignored `agents/runtime/` carve-out above. Exactly one
|
|
731
|
+
// path, never a prefix: a sibling like `<path>.bak` is still a
|
|
732
|
+
// real broken reference.
|
|
733
|
+
resolved = true;
|
|
734
|
+
}
|
|
735
|
+
if (
|
|
736
|
+
!resolved &&
|
|
737
|
+
_is_frozen_record_source(_relPosix(root, filepath)) &&
|
|
738
|
+
_resolves_under_terminal_disposition(rawRef, root)
|
|
739
|
+
) {
|
|
740
|
+
// A record naming the pre-archival path of a roadmap that is
|
|
741
|
+
// still findable. Nothing may rewrite it — see above.
|
|
742
|
+
resolved = true;
|
|
743
|
+
}
|
|
667
744
|
if (!resolved) {
|
|
668
745
|
broken.push(
|
|
669
746
|
makeBrokenRef(
|
|
@@ -1108,6 +1185,7 @@ export {
|
|
|
1108
1185
|
SKIP_NAMES,
|
|
1109
1186
|
EXAMPLE_PATH_PATTERNS,
|
|
1110
1187
|
ALLOWLIST_PATTERNS,
|
|
1188
|
+
UNTRACKED_GENERATED_DASHBOARD,
|
|
1111
1189
|
type AllowlistPattern,
|
|
1112
1190
|
_is_allowlisted,
|
|
1113
1191
|
collect_artifacts,
|