@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,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ADR-236 rule partition, decided per HOST directory on that host's own
|
|
3
|
+
* evidence.
|
|
4
|
+
*
|
|
5
|
+
* ## Why the decision moved from per-run to per-directory
|
|
6
|
+
*
|
|
7
|
+
* The filter used to sit in `condense.ts::_scoped_rule_basenames()`, which runs
|
|
8
|
+
* once per generation, so `partitionActive` — a claude-only fingerprint against
|
|
9
|
+
* `installed.lock` — decided for every host at once. That contradicted
|
|
10
|
+
* `partitionEligibility.ts`'s own fail-safe reasoning, which refuses to withhold a
|
|
11
|
+
* cursor artefact on claude's evidence because it "would deliver it nowhere", and
|
|
12
|
+
* it was measurably wrong in both directions on 2026-08-22 (partition ACTIVE,
|
|
13
|
+
* freshly generated worktree):
|
|
14
|
+
*
|
|
15
|
+
* ```
|
|
16
|
+
* .claude/rules 13 files, 13 package-only, 0 global-only
|
|
17
|
+
* .clinerules 14 files, 13 package-only, 0 global-only ← narrowed on
|
|
18
|
+
* borrowed evidence
|
|
19
|
+
* .cursor/rules 126 files, 26 package-only, 100 global-only ← never narrowed
|
|
20
|
+
* .windsurf/rules 113 files, 13 package-only, 100 global-only ← never narrowed
|
|
21
|
+
* .augment/rules 118 files, 15 package-only, 103 global-only ← never narrowed
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* So the narrowing is per directory and gated on real evidence:
|
|
25
|
+
* {@link hostLayerCarries} reads that host's global directory and answers whether
|
|
26
|
+
* every name about to be withheld is actually there. No layer, an unreadable layer,
|
|
27
|
+
* or one missing a single name → the full projection is returned. A withhold now
|
|
28
|
+
* costs a directory read; the alternative cost a rule.
|
|
29
|
+
*
|
|
30
|
+
* ## Why it lives here rather than in condense.ts
|
|
31
|
+
*
|
|
32
|
+
* Four call sites consume it — the symlink emit plan, the cursor `.mdc` emitter,
|
|
33
|
+
* the windsurf emitter, the augment projector — and four sites deciding this
|
|
34
|
+
* separately is exactly how three of them came to disagree. It is in `src/install/`
|
|
35
|
+
* beside {@link partitionActive} and {@link hostLayerCarries}, the two predicates
|
|
36
|
+
* it composes, rather than in the 2,700-line generator that calls it.
|
|
37
|
+
*
|
|
38
|
+
* ## The classification source is the PROJECTION, not the authored tree
|
|
39
|
+
*
|
|
40
|
+
* `rulesSource` is `dist/agent-src/rules` at every call site, deliberately. A rule
|
|
41
|
+
* absent from `dist/` is compile-disabled and never projected, so it can be neither
|
|
42
|
+
* withheld nor duplicated — classifying from `src/rules` would count
|
|
43
|
+
* `telegraph-speak.md` (compile-disabled: 119 rules / 16 package-only in `src/`,
|
|
44
|
+
* 118 / 15 in `dist/`) and disagree with the emitter by exactly one file.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import * as fs from 'node:fs';
|
|
48
|
+
import * as os from 'node:os';
|
|
49
|
+
import * as path from 'node:path';
|
|
50
|
+
|
|
51
|
+
import { isExclusivelyPackageOnly, partitionActive } from './partitionEligibility.js';
|
|
52
|
+
import { hostLayerCarries, toolIdForProjectRuleDir } from './globalRuleLayers.js';
|
|
53
|
+
|
|
54
|
+
export interface PartitionRulesOptions {
|
|
55
|
+
/** Project-relative host directory, e.g. `.cursor/rules`. */
|
|
56
|
+
readonly toolDir: string;
|
|
57
|
+
/** Candidate rule basenames, already scope- and manual-filtered. */
|
|
58
|
+
readonly rules: readonly string[];
|
|
59
|
+
/** Absolute project root — what `partitionActive` verifies against. */
|
|
60
|
+
readonly projectRoot: string;
|
|
61
|
+
/** Absolute directory the basenames resolve against for classification. */
|
|
62
|
+
readonly rulesSource: string;
|
|
63
|
+
readonly userHome?: string;
|
|
64
|
+
/** Where the kept-full notice goes. Silent when omitted. */
|
|
65
|
+
readonly announce?: (message: string) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Override the partition-active decision instead of reading `installed.lock`.
|
|
68
|
+
*
|
|
69
|
+
* Split out for the reason `partitionEligibility.ts` states about its own
|
|
70
|
+
* `personaWithheldFor`: "so the decision is testable in BOTH directions". A
|
|
71
|
+
* test over the un-injected path can only assert whatever the machine happens
|
|
72
|
+
* to be — and this one bit differs between a maintainer checkout (installed,
|
|
73
|
+
* so active) and CI (nothing installed, so inactive), which made the first
|
|
74
|
+
* version of `rule_partition_per_host.test.ts` green locally and red on every
|
|
75
|
+
* CI shard. That is the environment-dependent gate this whole change exists to
|
|
76
|
+
* remove, reproduced in its own test suite.
|
|
77
|
+
*
|
|
78
|
+
* Production callers omit it.
|
|
79
|
+
*/
|
|
80
|
+
readonly active?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Narrow one directory's rule list to the package-only set — but only when THIS
|
|
85
|
+
* host's global layer is verified to carry what would be withheld.
|
|
86
|
+
*
|
|
87
|
+
* Returns a copy of `rules` unchanged in every uncertain case. That direction is
|
|
88
|
+
* the whole design: an unnecessary duplicate costs context, a wrong withhold costs
|
|
89
|
+
* the rule.
|
|
90
|
+
*/
|
|
91
|
+
export function partitionRulesForDir(opts: PartitionRulesOptions): string[] {
|
|
92
|
+
const { toolDir, rules, projectRoot, rulesSource } = opts;
|
|
93
|
+
const active = opts.active ?? partitionActive(projectRoot);
|
|
94
|
+
if (!active) {
|
|
95
|
+
return [...rules];
|
|
96
|
+
}
|
|
97
|
+
const toolId = toolIdForProjectRuleDir(toolDir);
|
|
98
|
+
if (toolId === null) {
|
|
99
|
+
// An unmapped directory has no global layer this code can point at, so
|
|
100
|
+
// there is no evidence to withhold on.
|
|
101
|
+
return [...rules];
|
|
102
|
+
}
|
|
103
|
+
const packageOnly = rules.filter((r) => isExclusivelyPackageOnly(path.join(rulesSource, r)));
|
|
104
|
+
const keep = new Set(packageOnly);
|
|
105
|
+
const withheld = rules.filter((r) => !keep.has(r));
|
|
106
|
+
const home = opts.userHome ?? process.env['HOME'] ?? os.homedir();
|
|
107
|
+
const verdict = hostLayerCarries(toolId, withheld, home);
|
|
108
|
+
if (!verdict.carries) {
|
|
109
|
+
opts.announce?.(
|
|
110
|
+
` ⚠️ ${toolDir}: full projection kept — ${toolId}'s global layer ${verdict.reason}` +
|
|
111
|
+
(verdict.missing.length > 0
|
|
112
|
+
? ` (${String(verdict.missing.length)} rule(s) not there, e.g. ` +
|
|
113
|
+
`${verdict.missing.slice(0, 3).join(', ')})`
|
|
114
|
+
: ''),
|
|
115
|
+
);
|
|
116
|
+
return [...rules];
|
|
117
|
+
}
|
|
118
|
+
return packageOnly;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── The older, weaker duplicate-avoidance mechanism ──────────────────────────
|
|
122
|
+
//
|
|
123
|
+
// `projection.scope_dedup` (default OFF) predates the partition and solves the
|
|
124
|
+
// same problem with a narrower method: skip a project copy only when its
|
|
125
|
+
// user-scope twin is BYTE-IDENTICAL. It moved here from condense.ts alongside the
|
|
126
|
+
// partition because the two are one subject — a reader deciding how a duplicate is
|
|
127
|
+
// avoided should find both in one file rather than one here and one in a
|
|
128
|
+
// 2,700-line generator. Behaviour is unchanged; only `MODULE_STATE` reads became
|
|
129
|
+
// parameters.
|
|
130
|
+
|
|
131
|
+
/** `fs.statSync(p).isFile()`, false on any error — condense.ts's `_isFile`. */
|
|
132
|
+
function isFile(p: string): boolean {
|
|
133
|
+
try {
|
|
134
|
+
return fs.statSync(p).isFile();
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Per-tool user-scope rule directory, for the scope-dedup below. Only tools
|
|
142
|
+
* whose host reads a user-scope rules directory can have a redundant twin;
|
|
143
|
+
* everything else has nothing to de-duplicate against.
|
|
144
|
+
*/
|
|
145
|
+
const USER_SCOPE_RULE_DIRS: Readonly<Record<string, string>> = {
|
|
146
|
+
'.claude/rules': path.join('.claude', 'rules'),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Rules whose user-scope twin is BYTE-IDENTICAL to the source, i.e. safe to
|
|
151
|
+
* skip at project scope: the host still loads the same text, once instead of
|
|
152
|
+
* twice, so nothing the model sees changes.
|
|
153
|
+
*
|
|
154
|
+
* Byte-identity is the whole safety argument and is not negotiable. Keying on
|
|
155
|
+
* the filename alone would silently let a stale globally-installed copy win
|
|
156
|
+
* whenever the two scopes hold different versions — which is the NORMAL state
|
|
157
|
+
* while developing the package (measured: 110/110 shared filenames differing in
|
|
158
|
+
* bytes). This is deliberately not the thin-projection mechanism: no rule
|
|
159
|
+
* becomes trigger-gated, so the quality floor that disabled thin projection
|
|
160
|
+
* does not apply here.
|
|
161
|
+
*/
|
|
162
|
+
export interface DedupOptions {
|
|
163
|
+
readonly toolDir: string;
|
|
164
|
+
readonly rules: readonly string[];
|
|
165
|
+
readonly userHome: string;
|
|
166
|
+
/** Absolute directory the basenames resolve against, for the byte compare. */
|
|
167
|
+
readonly rulesSource: string;
|
|
168
|
+
readonly announce?: (message: string) => void;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function dedupableRules(opts: DedupOptions): Set<string> {
|
|
172
|
+
const { toolDir: tool_dir, rules, userHome, rulesSource } = opts;
|
|
173
|
+
const relative = USER_SCOPE_RULE_DIRS[tool_dir];
|
|
174
|
+
if (relative === undefined) {
|
|
175
|
+
return new Set();
|
|
176
|
+
}
|
|
177
|
+
// A hostile or simply absent $HOME must make the dedup inert, not
|
|
178
|
+
// adventurous. In a container `$HOME` is often unset (so `homedir()` can
|
|
179
|
+
// resolve to `/`) or world-writable, and this function decides which rules
|
|
180
|
+
// to STOP emitting — reading an unexpected tree there is how a projection
|
|
181
|
+
// silently loses a rule. Council review of PR #1055 raised exactly this.
|
|
182
|
+
let userDirStat: fs.Stats;
|
|
183
|
+
const userDir = path.join(userHome, relative);
|
|
184
|
+
try {
|
|
185
|
+
userDirStat = fs.statSync(userDir);
|
|
186
|
+
} catch {
|
|
187
|
+
return new Set();
|
|
188
|
+
}
|
|
189
|
+
if (!userDirStat.isDirectory()) {
|
|
190
|
+
return new Set();
|
|
191
|
+
}
|
|
192
|
+
// World-writable user scope: anyone on the box could plant a byte-identical
|
|
193
|
+
// twin and thereby delete a rule from the project projection. Refuse.
|
|
194
|
+
if ((userDirStat.mode & 0o002) !== 0) {
|
|
195
|
+
opts.announce?.(` ⚠️ ${tool_dir}: user-scope rules dir is world-writable — scope-dedup skipped`);
|
|
196
|
+
return new Set();
|
|
197
|
+
}
|
|
198
|
+
const skip = new Set<string>();
|
|
199
|
+
for (const rule of rules) {
|
|
200
|
+
const twin = path.join(userDir, rule);
|
|
201
|
+
const source = path.join(rulesSource, rule);
|
|
202
|
+
try {
|
|
203
|
+
if (!isFile(twin) || !isFile(source)) continue;
|
|
204
|
+
if (fs.readFileSync(twin).equals(fs.readFileSync(source))) {
|
|
205
|
+
skip.add(rule);
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// An unreadable twin is simply not de-duplicable — emit the project copy.
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return skip;
|
|
212
|
+
}
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agent-config adr:effective ADR-NNN` — the effective state of one decision.
|
|
3
|
+
*
|
|
4
|
+
* Step 2.2 of `road-to-evidence-based-adr-governance`, Phase 2 — surfacing
|
|
5
|
+
* only. Nothing here gates, permits, or denies anything: the verb reports what
|
|
6
|
+
* a record says about itself and exits 1 only when the record contradicts its
|
|
7
|
+
* OWN amendment, which is a defect in the document, never a verdict about
|
|
8
|
+
* whether a transition may proceed.
|
|
9
|
+
*
|
|
10
|
+
* ## The defect it closes
|
|
11
|
+
*
|
|
12
|
+
* A long ADR is read linearly, so a reader meets whichever half comes first.
|
|
13
|
+
* `adr-layout § Amendments` records the measured cost twice over: ADR-035
|
|
14
|
+
* asserted in two places a rejection ADR-232 had reopened, and the contract
|
|
15
|
+
* paragraph naming that failure was itself stale by the time it was read. The
|
|
16
|
+
* live fixture is ADR-020, whose 2026-07-13 amendment retired the committed
|
|
17
|
+
* bridge marker while its Decision still mandates writing one and its
|
|
18
|
+
* Consequences still narrate it as a live failure mode.
|
|
19
|
+
*
|
|
20
|
+
* ## Why a declaration and not an inference
|
|
21
|
+
*
|
|
22
|
+
* "Which clauses did this amendment supersede" is a semantic question, and this
|
|
23
|
+
* roadmap's own architecture forbids answering it by guess — an agent grading
|
|
24
|
+
* its own homework is the failure round 5 refused one layer up. So the record
|
|
25
|
+
* declares it, in two literals that read as prose and parse deterministically:
|
|
26
|
+
*
|
|
27
|
+
* ```markdown
|
|
28
|
+
* ## Amendment — 2026-07-13 · bridge marker retired
|
|
29
|
+
*
|
|
30
|
+
* > retires: `agents/.event4u-bridge.yml`, `bridge marker`
|
|
31
|
+
*
|
|
32
|
+
* ... elsewhere, in the clause the amendment overtook:
|
|
33
|
+
*
|
|
34
|
+
* **(Superseded by the 2026-07-13 amendment — the marker is not written.)**
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* A marked clause is reported as superseded. A clause in an ASSERTIVE section
|
|
38
|
+
* naming a retired token with no marker is a contradiction. Absent a `retires:`
|
|
39
|
+
* declaration the verb reports amendments, axes and trigger state and claims
|
|
40
|
+
* nothing about clauses — which is the honest output for the 17 other amended
|
|
41
|
+
* records, not a silent pass.
|
|
42
|
+
*
|
|
43
|
+
* ## Why only the assertive sections
|
|
44
|
+
*
|
|
45
|
+
* `Context`, `Alternatives considered` and `References` describe the situation
|
|
46
|
+
* as it stood when the decision was taken; a retired term appearing there is
|
|
47
|
+
* the history the ADR exists to keep, not a live claim. `adr-layout` frames the
|
|
48
|
+
* defect the same way — "an ADR whose top half still asserts what its own
|
|
49
|
+
* amendment reversed" — so the scan covers `Decision`, `Consequences` and
|
|
50
|
+
* earlier amendment blocks, and says so rather than reporting the whole file
|
|
51
|
+
* and burying the three lines that matter.
|
|
52
|
+
*
|
|
53
|
+
* Exit codes: 0 clean · 1 a contradiction, or the ref resolved nowhere · 2 usage.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import * as fs from 'node:fs';
|
|
57
|
+
import * as path from 'node:path';
|
|
58
|
+
import process from 'node:process';
|
|
59
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
60
|
+
|
|
61
|
+
import {
|
|
62
|
+
adr_files,
|
|
63
|
+
normalise_ref,
|
|
64
|
+
parse_frontmatter,
|
|
65
|
+
trigger_state,
|
|
66
|
+
type TriggerState,
|
|
67
|
+
} from '../adr_cite_check.js';
|
|
68
|
+
import {
|
|
69
|
+
authorityBasisOf,
|
|
70
|
+
evidenceOf,
|
|
71
|
+
provenanceOf,
|
|
72
|
+
readAdrFrontmatter,
|
|
73
|
+
} from '../_lib/adr_frontmatter.js';
|
|
74
|
+
|
|
75
|
+
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
76
|
+
const REPO_ROOT = path.resolve(_HERE, '..', '..', '..');
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sections whose clauses are read as live assertions.
|
|
80
|
+
*
|
|
81
|
+
* Matched on the `## ` heading's leading word so the three amendment
|
|
82
|
+
* conventions in the corpus (`## Amendment N (date)`, `## Amendment — date ·
|
|
83
|
+
* topic`, plus the `**Amended <date> —` inline form) all land in the set.
|
|
84
|
+
*/
|
|
85
|
+
export const ASSERTIVE_SECTIONS = ['decision', 'consequences', 'amendment'] as const;
|
|
86
|
+
|
|
87
|
+
/** `> retires: \`a\`, \`b\`` — the tokens an amendment block declares retired. */
|
|
88
|
+
const RETIRES_RE = /^>?\s*retires:\s*(.+)$/i;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A clause-level supersession marker.
|
|
92
|
+
*
|
|
93
|
+
* `Superseded`, `Narrowed` and `Reopened` are all accepted: the corpus already
|
|
94
|
+
* carries the third (`ADR-035:47`, `**(Reopened by ADR-232 — see the banner
|
|
95
|
+
* above.)**`) and inventing a fourth spelling for the same act would leave that
|
|
96
|
+
* marker unread by the one tool written to read markers.
|
|
97
|
+
*/
|
|
98
|
+
const CLAUSE_MARKER_RE = /\*\*\((?:Superseded|Narrowed|Reopened) by ([^—)]+?)\s*(?:—[^)]*)?\)\*\*/;
|
|
99
|
+
|
|
100
|
+
export interface AmendmentBlock {
|
|
101
|
+
/** The heading line, verbatim minus the leading `#`s. */
|
|
102
|
+
heading: string;
|
|
103
|
+
/** 1-based line number of the heading. */
|
|
104
|
+
line: number;
|
|
105
|
+
/** Tokens this block declares retired, verbatim (backticks stripped). */
|
|
106
|
+
retires: string[];
|
|
107
|
+
/** 1-based line range the block spans, end inclusive. */
|
|
108
|
+
range: [number, number];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface ClauseFinding {
|
|
112
|
+
/** 1-based line number of the clause's first line. */
|
|
113
|
+
line: number;
|
|
114
|
+
/** The `## ` heading the clause sits under, or `(frontmatter)`. */
|
|
115
|
+
section: string;
|
|
116
|
+
/** The retired token, or the marker's referent for a marked clause. */
|
|
117
|
+
subject: string;
|
|
118
|
+
/** The clause's first line, trimmed — enough to locate it by eye. */
|
|
119
|
+
excerpt: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface EffectiveState {
|
|
123
|
+
ref: string;
|
|
124
|
+
resolved: boolean;
|
|
125
|
+
file?: string;
|
|
126
|
+
status?: string;
|
|
127
|
+
date?: string;
|
|
128
|
+
decision?: string;
|
|
129
|
+
provenance_kind?: string;
|
|
130
|
+
evidence_strength?: string;
|
|
131
|
+
evidence_discovery?: string;
|
|
132
|
+
authority_basis?: string;
|
|
133
|
+
review_trigger?: string;
|
|
134
|
+
trigger_state: TriggerState;
|
|
135
|
+
amended_by?: string;
|
|
136
|
+
amendments: AmendmentBlock[];
|
|
137
|
+
/** Retired tokens declared across every amendment block, deduplicated. */
|
|
138
|
+
retired_tokens: string[];
|
|
139
|
+
/** Clauses carrying a supersession marker. */
|
|
140
|
+
superseded_clauses: ClauseFinding[];
|
|
141
|
+
/** Assertive clauses naming a retired token with no marker. */
|
|
142
|
+
contradictions: ClauseFinding[];
|
|
143
|
+
/** The Decision section, verbatim, as the effective text to read. */
|
|
144
|
+
effective_decision: string[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface Clause {
|
|
148
|
+
line: number;
|
|
149
|
+
section: string;
|
|
150
|
+
text: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Strip backticks and surrounding whitespace off one declared token. */
|
|
154
|
+
function cleanToken(raw: string): string {
|
|
155
|
+
return raw.trim().replace(/^`+|`+$/g, '').trim();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The heading a line sits under, `(preamble)` before the first one. */
|
|
159
|
+
function sectionOf(headings: { line: number; text: string }[], line: number): string {
|
|
160
|
+
let current = '(preamble)';
|
|
161
|
+
for (const h of headings) {
|
|
162
|
+
if (h.line > line) break;
|
|
163
|
+
current = h.text;
|
|
164
|
+
}
|
|
165
|
+
return current;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isAssertive(section: string): boolean {
|
|
169
|
+
const first = section.toLowerCase().replace(/^#+\s*/, '').split(/[\s·—-]/)[0] ?? '';
|
|
170
|
+
return (ASSERTIVE_SECTIONS as readonly string[]).includes(first);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Split a document body into clauses.
|
|
175
|
+
*
|
|
176
|
+
* A clause ends at a blank line OR at the start of the next list item, and the
|
|
177
|
+
* second half is load-bearing: a run of bullets with no blank line between them
|
|
178
|
+
* is one contiguous block, so paragraph-only splitting would let a marker on
|
|
179
|
+
* one bullet clear the bullet below it.
|
|
180
|
+
*/
|
|
181
|
+
export function splitClauses(lines: string[], headings: { line: number; text: string }[]): Clause[] {
|
|
182
|
+
const out: Clause[] = [];
|
|
183
|
+
let start = -1;
|
|
184
|
+
let buf: string[] = [];
|
|
185
|
+
const flush = (): void => {
|
|
186
|
+
if (start === -1) return;
|
|
187
|
+
out.push({ line: start, section: sectionOf(headings, start), text: buf.join('\n') });
|
|
188
|
+
start = -1;
|
|
189
|
+
buf = [];
|
|
190
|
+
};
|
|
191
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
192
|
+
const raw = lines[i] as string;
|
|
193
|
+
const num = i + 1;
|
|
194
|
+
if (raw.trim() === '') {
|
|
195
|
+
flush();
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (/^\s*(?:[-*+]\s|\d+\.\s)/.test(raw)) flush();
|
|
199
|
+
if (start === -1) start = num;
|
|
200
|
+
buf.push(raw);
|
|
201
|
+
}
|
|
202
|
+
flush();
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Every `## `/`### ` heading, plus the inline `**Amended <date>` convention. */
|
|
207
|
+
function headingsOf(lines: string[]): { line: number; text: string }[] {
|
|
208
|
+
const out: { line: number; text: string }[] = [];
|
|
209
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
210
|
+
const raw = lines[i] as string;
|
|
211
|
+
if (/^#{2,3}\s+/.test(raw)) out.push({ line: i + 1, text: raw.replace(/^#+\s*/, '').trim() });
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Amendment blocks with their extents and declared retirements. */
|
|
217
|
+
export function amendmentsOf(lines: string[]): AmendmentBlock[] {
|
|
218
|
+
const headings = headingsOf(lines);
|
|
219
|
+
const out: AmendmentBlock[] = [];
|
|
220
|
+
for (let h = 0; h < headings.length; h += 1) {
|
|
221
|
+
const heading = headings[h] as { line: number; text: string };
|
|
222
|
+
if (!/^amend/i.test(heading.text)) continue;
|
|
223
|
+
const next = headings[h + 1];
|
|
224
|
+
const end = next === undefined ? lines.length : next.line - 1;
|
|
225
|
+
const retires: string[] = [];
|
|
226
|
+
for (let i = heading.line; i < end; i += 1) {
|
|
227
|
+
const m = RETIRES_RE.exec((lines[i] as string).trim());
|
|
228
|
+
if (m?.[1] === undefined) continue;
|
|
229
|
+
for (const part of m[1].split(',')) {
|
|
230
|
+
const token = cleanToken(part);
|
|
231
|
+
if (token !== '') retires.push(token);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
out.push({ heading: heading.text, line: heading.line, retires, range: [heading.line, end] });
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The verbatim `## Decision` section, or `[]` when the record has none. */
|
|
240
|
+
export function decisionSection(lines: string[]): string[] {
|
|
241
|
+
const headings = headingsOf(lines);
|
|
242
|
+
for (let h = 0; h < headings.length; h += 1) {
|
|
243
|
+
const heading = headings[h] as { line: number; text: string };
|
|
244
|
+
if (!/^decision\b/i.test(heading.text)) continue;
|
|
245
|
+
const next = headings[h + 1];
|
|
246
|
+
const end = next === undefined ? lines.length : next.line - 1;
|
|
247
|
+
return lines.slice(heading.line, end).filter((l, idx, all) => !(l.trim() === '' && all[idx + 1] === undefined));
|
|
248
|
+
}
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Superseded clauses and contradictions, from the declarations alone.
|
|
254
|
+
*
|
|
255
|
+
* A clause inside the amendment block that declares the retirement is skipped:
|
|
256
|
+
* naming the thing it retires is what an amendment does.
|
|
257
|
+
*/
|
|
258
|
+
export function clauseFindings(
|
|
259
|
+
lines: string[],
|
|
260
|
+
amendments: AmendmentBlock[],
|
|
261
|
+
): { superseded: ClauseFinding[]; contradictions: ClauseFinding[] } {
|
|
262
|
+
const headings = headingsOf(lines);
|
|
263
|
+
const clauses = splitClauses(lines, headings);
|
|
264
|
+
const superseded: ClauseFinding[] = [];
|
|
265
|
+
const contradictions: ClauseFinding[] = [];
|
|
266
|
+
|
|
267
|
+
for (const clause of clauses) {
|
|
268
|
+
const marker = CLAUSE_MARKER_RE.exec(clause.text);
|
|
269
|
+
const excerpt = (clause.text.split('\n')[0] ?? '').trim();
|
|
270
|
+
if (marker?.[1] !== undefined) {
|
|
271
|
+
superseded.push({
|
|
272
|
+
line: clause.line,
|
|
273
|
+
section: clause.section,
|
|
274
|
+
subject: marker[1].trim(),
|
|
275
|
+
excerpt,
|
|
276
|
+
});
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (!isAssertive(clause.section)) continue;
|
|
280
|
+
for (const amendment of amendments) {
|
|
281
|
+
const inDeclaringBlock =
|
|
282
|
+
clause.line >= amendment.range[0] && clause.line <= amendment.range[1];
|
|
283
|
+
if (inDeclaringBlock) continue;
|
|
284
|
+
for (const token of amendment.retires) {
|
|
285
|
+
if (!clause.text.includes(token)) continue;
|
|
286
|
+
contradictions.push({
|
|
287
|
+
line: clause.line,
|
|
288
|
+
section: clause.section,
|
|
289
|
+
subject: token,
|
|
290
|
+
excerpt,
|
|
291
|
+
});
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return { superseded, contradictions };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Resolve one reference to a decision file on a scanned surface. */
|
|
300
|
+
export function resolveAdrFile(ref: string, repoRoot: string): string | null {
|
|
301
|
+
const parsed = normalise_ref(ref);
|
|
302
|
+
if (parsed === null) return null;
|
|
303
|
+
const files = adr_files(repoRoot);
|
|
304
|
+
const { area, num } = parsed;
|
|
305
|
+
const match =
|
|
306
|
+
area === null
|
|
307
|
+
? files.find((f) => new RegExp(`(^|/)ADR-0*${num}[-.]`, 'i').test(f))
|
|
308
|
+
: files.find((f) => new RegExp(`/docs/adrs/${area}/0*${String(Number(num))}-`, 'i').test(f));
|
|
309
|
+
return match ?? null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function effectiveState(ref: string, repoRoot: string = REPO_ROOT): EffectiveState {
|
|
313
|
+
const parsed = normalise_ref(ref);
|
|
314
|
+
const id = parsed?.id ?? ref;
|
|
315
|
+
const match = resolveAdrFile(ref, repoRoot);
|
|
316
|
+
if (match === null) {
|
|
317
|
+
return {
|
|
318
|
+
ref: id,
|
|
319
|
+
resolved: false,
|
|
320
|
+
trigger_state: 'none',
|
|
321
|
+
amendments: [],
|
|
322
|
+
retired_tokens: [],
|
|
323
|
+
superseded_clauses: [],
|
|
324
|
+
contradictions: [],
|
|
325
|
+
effective_decision: [],
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const text = fs.readFileSync(match, 'utf-8');
|
|
330
|
+
const scalars = parse_frontmatter(text) ?? {};
|
|
331
|
+
const structured = readAdrFrontmatter(text);
|
|
332
|
+
const provenance = structured === null ? null : provenanceOf(structured);
|
|
333
|
+
const evidence = structured === null ? null : evidenceOf(structured);
|
|
334
|
+
const basis = structured === null ? null : authorityBasisOf(structured);
|
|
335
|
+
|
|
336
|
+
const bodyStart = text.indexOf('\n---\n', 4);
|
|
337
|
+
const frontmatterLines = bodyStart === -1 ? 0 : text.slice(0, bodyStart + 5).split('\n').length - 1;
|
|
338
|
+
const lines = text.split('\n');
|
|
339
|
+
const amendments = amendmentsOf(lines).filter((a) => a.line > frontmatterLines);
|
|
340
|
+
const { superseded, contradictions } = clauseFindings(lines, amendments);
|
|
341
|
+
const retired = [...new Set(amendments.flatMap((a) => a.retires))];
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
ref: id,
|
|
345
|
+
resolved: true,
|
|
346
|
+
file: path.relative(repoRoot, match),
|
|
347
|
+
...(scalars['status'] !== undefined ? { status: scalars['status'] } : {}),
|
|
348
|
+
...(scalars['date'] !== undefined ? { date: scalars['date'] } : {}),
|
|
349
|
+
...(scalars['decision'] !== undefined ? { decision: scalars['decision'] } : {}),
|
|
350
|
+
...(provenance?.kind != null ? { provenance_kind: provenance.kind } : {}),
|
|
351
|
+
...(evidence?.strength != null ? { evidence_strength: evidence.strength } : {}),
|
|
352
|
+
...(evidence?.discovery != null ? { evidence_discovery: evidence.discovery } : {}),
|
|
353
|
+
...(basis !== null ? { authority_basis: basis } : {}),
|
|
354
|
+
...(scalars['review_trigger'] !== undefined && scalars['review_trigger'] !== ''
|
|
355
|
+
? { review_trigger: scalars['review_trigger'] }
|
|
356
|
+
: {}),
|
|
357
|
+
trigger_state: trigger_state(scalars),
|
|
358
|
+
...(scalars['amended_by'] !== undefined && scalars['amended_by'] !== '—'
|
|
359
|
+
? { amended_by: scalars['amended_by'] }
|
|
360
|
+
: {}),
|
|
361
|
+
amendments,
|
|
362
|
+
retired_tokens: retired,
|
|
363
|
+
superseded_clauses: superseded,
|
|
364
|
+
contradictions,
|
|
365
|
+
effective_decision: decisionSection(lines),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function render(state: EffectiveState): string[] {
|
|
370
|
+
const out: string[] = [];
|
|
371
|
+
out.push(`${state.ref}${state.file !== undefined ? ` · ${state.file}` : ''}`);
|
|
372
|
+
if (!state.resolved) {
|
|
373
|
+
out.push(' ❌ UNRESOLVED — no decision record carries this number on a scanned surface.');
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
out.push(` status ${state.status ?? '—'} date ${state.date ?? '—'}`);
|
|
377
|
+
if (state.decision !== undefined) out.push(` decision ${state.decision}`);
|
|
378
|
+
out.push(` provenance ${state.provenance_kind ?? '— (no provenance axis)'}`);
|
|
379
|
+
out.push(
|
|
380
|
+
` evidence ${state.evidence_strength ?? '— (ungraded)'}` +
|
|
381
|
+
` · discovery ${state.evidence_discovery ?? '—'}`,
|
|
382
|
+
);
|
|
383
|
+
out.push(` authority_basis ${state.authority_basis ?? '— (absent → evidence)'}`);
|
|
384
|
+
out.push(` review_trigger ${state.review_trigger ?? '— (none recorded)'}`);
|
|
385
|
+
out.push(` trigger state ${state.trigger_state}`);
|
|
386
|
+
if (state.amended_by !== undefined) out.push(` amended_by ${state.amended_by}`);
|
|
387
|
+
|
|
388
|
+
out.push('', ' active amendments');
|
|
389
|
+
if (state.amendments.length === 0) {
|
|
390
|
+
out.push(' — none in the body');
|
|
391
|
+
} else {
|
|
392
|
+
for (const a of state.amendments) {
|
|
393
|
+
const declares =
|
|
394
|
+
a.retires.length > 0 ? ` · retires ${a.retires.map((t) => `\`${t}\``).join(', ')}` : '';
|
|
395
|
+
out.push(` :${String(a.line)} ${a.heading}${declares}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
out.push('', ' superseded clauses');
|
|
400
|
+
if (state.retired_tokens.length === 0 && state.superseded_clauses.length === 0) {
|
|
401
|
+
out.push(' — no amendment declares a `retires:` list and no clause carries a marker,');
|
|
402
|
+
out.push(' so nothing is claimed about this record\'s clauses. Read the amendments.');
|
|
403
|
+
} else if (state.superseded_clauses.length === 0) {
|
|
404
|
+
out.push(' — none marked');
|
|
405
|
+
} else {
|
|
406
|
+
for (const c of state.superseded_clauses) {
|
|
407
|
+
out.push(` :${String(c.line)} ${c.section} — superseded by ${c.subject}`);
|
|
408
|
+
out.push(` ${c.excerpt}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
out.push('', ' effective decision');
|
|
413
|
+
if (state.effective_decision.length === 0) {
|
|
414
|
+
out.push(' — no `## Decision` section');
|
|
415
|
+
} else {
|
|
416
|
+
for (const line of state.effective_decision) out.push(` ${line}`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (state.contradictions.length > 0) {
|
|
420
|
+
out.push('', ' ❌ current-vs-amendment contradiction');
|
|
421
|
+
for (const c of state.contradictions) {
|
|
422
|
+
out.push(` :${String(c.line)} ${c.section} asserts \`${c.subject}\`, which an`);
|
|
423
|
+
out.push(' amendment on this record declares retired, and carries no marker.');
|
|
424
|
+
out.push(` ${c.excerpt}`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
interface ParsedArgv {
|
|
431
|
+
ok: boolean;
|
|
432
|
+
message?: string;
|
|
433
|
+
refs?: string[];
|
|
434
|
+
json?: boolean;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function parseArgv(argv: readonly string[]): ParsedArgv {
|
|
438
|
+
let json = false;
|
|
439
|
+
const refs: string[] = [];
|
|
440
|
+
for (const a of argv) {
|
|
441
|
+
if (a === '--json') json = true;
|
|
442
|
+
else if (a === '-h' || a === '--help')
|
|
443
|
+
return { ok: false, message: 'usage: agent-config adr:effective <ADR-NNN> [ADR-NNN …] [--json]' };
|
|
444
|
+
else if (a.startsWith('--')) return { ok: false, message: `unknown argument: ${a}` };
|
|
445
|
+
else refs.push(a);
|
|
446
|
+
}
|
|
447
|
+
if (refs.length === 0)
|
|
448
|
+
return { ok: false, message: 'usage: agent-config adr:effective <ADR-NNN> [ADR-NNN …] [--json]' };
|
|
449
|
+
return { ok: true, refs, json };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
453
|
+
const parsed = parseArgv(argv);
|
|
454
|
+
if (!parsed.ok) {
|
|
455
|
+
process.stderr.write(`${parsed.message ?? 'usage error'}\n`);
|
|
456
|
+
return 2;
|
|
457
|
+
}
|
|
458
|
+
const states = (parsed.refs ?? []).map((ref) => effectiveState(ref));
|
|
459
|
+
if (parsed.json === true) {
|
|
460
|
+
process.stdout.write(`${JSON.stringify(states, null, 2)}\n`);
|
|
461
|
+
} else {
|
|
462
|
+
for (const state of states) {
|
|
463
|
+
for (const line of render(state)) process.stdout.write(`${line}\n`);
|
|
464
|
+
process.stdout.write('\n');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const bad = states.filter((s) => !s.resolved || s.contradictions.length > 0);
|
|
468
|
+
return bad.length > 0 ? 1 : 0;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function _isCliEntry(): boolean {
|
|
472
|
+
if (process.argv[1] === undefined) return false;
|
|
473
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
474
|
+
if (import.meta.url === argvUrl) return true;
|
|
475
|
+
try {
|
|
476
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
477
|
+
} catch {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (_isCliEntry()) {
|
|
483
|
+
process.exitCode = main();
|
|
484
|
+
}
|