@event4u/agent-config 14.7.0 → 14.9.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 +175 -390
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +34 -4
- 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 +383 -0
- package/dist/agent-src/commands/review/changes.md +95 -14
- package/dist/agent-src/commands/roadmap/process-full.md +157 -3
- 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/host-capability-manifest.md +77 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +100 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +80 -0
- package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
- package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
- package/dist/agent-src/rules/recurring-criticism.md +1 -1
- package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
- package/dist/agent-src/rules/source-confidentiality.md +1 -1
- package/dist/agent-src/rules/untrusted-input-defense.md +9 -3
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
- package/dist/agent-src/skills/code-review/evals/evals.json +87 -46
- 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/humanizer/SKILL.md +39 -0
- package/dist/agent-src/skills/humanizer/evals/strip_fixtures.json +57 -0
- package/dist/agent-src/skills/humanizer/references/fixtures.md +54 -0
- package/dist/agent-src/skills/judge-spec-compliance/SKILL.md +154 -0
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +45 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +18 -18
- 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/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +28 -2
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-77GR3VD4.js} +68 -43
- package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
- package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-CYH5SP6M.js} +4 -23
- package/dist/cli-delegate/{chunk-Q26HT4QT.js → chunk-E46KPN6K.js} +251 -150
- package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
- package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
- package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
- package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-PLSG6UL5.js} +14 -14
- package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
- package/dist/cli-delegate/cmd_conformance.js +101 -25
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +7 -7
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_handoff.js +7 -6
- package/dist/cli-delegate/cmd_memory_get.js +2 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +5 -5
- package/dist/cli-delegate/cmd_prune.js +6 -6
- 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 +3 -3
- package/dist/cli-delegate/cmd_settings_get.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +83 -30
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +11 -9
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +9 -7
- package/dist/hooks/dispatch.js +1295 -812
- package/dist/install/globalRuleLayers.js +169 -0
- package/dist/install/globalRuleLayers.js.map +1 -0
- package/dist/install/partitionEligibility.js +156 -0
- package/dist/install/partitionEligibility.js.map +1 -1
- 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 +1358 -805
- package/dist/scripts/_lib/claude_plugin.js +162 -0
- package/dist/scripts/_lib/claude_plugin.js.map +1 -0
- package/dist/scripts/_lib/environment_detector.js +397 -0
- package/dist/scripts/_lib/environment_detector.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/_lib/spawn_env.js +129 -0
- package/dist/scripts/_lib/spawn_env.js.map +1 -0
- package/dist/scripts/ai_council/modes.js +199 -0
- package/dist/scripts/ai_council/modes.js.map +1 -0
- package/dist/scripts/ai_council/transport_resolver.js +418 -0
- package/dist/scripts/ai_council/transport_resolver.js.map +1 -0
- package/dist/scripts/condense.js +52 -89
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/routes/wizard.js +3 -2
- package/dist/server/routes/wizard.js.map +1 -1
- package/dist/ui/assets/index-Cxgy63kz.js +26 -0
- package/dist/ui/assets/index-Cxgy63kz.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
- package/docs/guidelines/agent-infra/false-green.md +47 -1
- package/docs/guidelines/agent-infra/traceability-field-mechanics.md +50 -0
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +17 -0
- package/package.json +7 -3
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/src/agent-src/scripts/dashboard_mode.ts +212 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/src/config/ci-local-parity.yml +21 -0
- package/src/config/estate-count-budget.json +13 -471
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +458 -12
- package/src/config/gate-violation-baselines.json +18 -8
- package/src/config/gitignore-block.txt +15 -2
- package/src/config/hook-latency-budget.json +5 -0
- package/src/config/publish-surface.json +60 -0
- package/src/config/requirements-trace-budget.json +32 -0
- package/src/config/rule-stub-ceilings.json +25 -4
- package/src/install/globalRuleLayers.ts +197 -0
- package/src/install/partitionEligibility.ts +177 -0
- package/src/install/ruleLayerPartition.ts +212 -0
- package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
- package/src/scripts/_dispatch.bash +18 -0
- package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
- package/src/scripts/_lib/adr_frontmatter.ts +459 -0
- package/src/scripts/_lib/base_tree.ts +175 -0
- package/src/scripts/_lib/billing_grant.ts +165 -0
- package/src/scripts/_lib/billing_grant_cli.ts +123 -0
- package/src/scripts/_lib/council_fallback_posture.ts +53 -6
- package/src/scripts/_lib/council_fallback_wiring.ts +61 -1
- package/src/scripts/_lib/envelope_grounding.ts +11 -1
- package/src/scripts/_lib/host_capability.ts +59 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
- package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
- package/src/scripts/_lib/quota_parked.ts +133 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +62 -0
- package/src/scripts/_lib/review_telemetry.ts +196 -0
- package/src/scripts/_lib/subagent_response.ts +92 -0
- package/src/scripts/adr/evidence_census.ts +899 -0
- package/src/scripts/adr/regenerate_index.ts +25 -38
- package/src/scripts/adr_cite_check.ts +540 -39
- package/src/scripts/ai_council/clients.ts +12 -1
- package/src/scripts/ai_council/config.ts +72 -115
- package/src/scripts/ai_council/fallback_config.ts +19 -7
- package/src/scripts/ai_council/mid_flight_fallback.ts +88 -7
- package/src/scripts/ai_council/orchestrator.ts +2 -2
- package/src/scripts/ai_council/py_format.ts +124 -0
- package/src/scripts/ai_council/transport_resolver.ts +65 -2
- package/src/scripts/audit_adr_coverage.ts +134 -21
- 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_council_pin_staleness.ts +300 -0
- package/src/scripts/check_estate_count.ts +420 -293
- package/src/scripts/check_gate_coverage.ts +91 -0
- package/src/scripts/check_hook_bundle_content.ts +219 -0
- package/src/scripts/check_new_adr_evidence.ts +529 -0
- package/src/scripts/check_no_stub_inventory_table.ts +292 -0
- package/src/scripts/check_pack_size.ts +234 -1
- package/src/scripts/check_publish_surface.ts +297 -0
- package/src/scripts/check_references.ts +18 -0
- package/src/scripts/check_requirements_trace.ts +378 -0
- package/src/scripts/check_rule_layer_partition.ts +531 -0
- package/src/scripts/check_secret_leak.ts +166 -12
- package/src/scripts/check_single_delivery.ts +137 -4
- package/src/scripts/check_standing_rule_delivery.ts +8 -2
- package/src/scripts/code_graph/cli.ts +44 -5
- package/src/scripts/code_graph/extract.ts +87 -0
- package/src/scripts/condense.ts +61 -91
- package/src/scripts/council_cli.ts +12 -16
- package/src/scripts/detect_ai_tells.ts +87 -0
- package/src/scripts/dispatch_r2_reviewer.ts +42 -0
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/generate_capabilities_index.ts +41 -0
- package/src/scripts/git_authorization_hook.ts +54 -4
- package/src/scripts/hooks/block_unauthorized_git.ts +41 -3
- 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/run_continuation_hook.ts +3 -1
- package/src/scripts/hooks/state_io.ts +88 -1
- package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +76 -11
- package/src/scripts/injection_scan_hook.ts +257 -15
- package/src/scripts/lint_adr_sweep_routing.ts +365 -0
- package/src/scripts/lint_hook_manifest.ts +49 -0
- package/src/scripts/lint_provenance_vocabulary.ts +472 -4
- package/src/scripts/lint_workflow_security.ts +53 -10
- package/src/scripts/lint_workflow_security_allowlist.json +2 -18
- package/src/scripts/report_envelope_rate.ts +166 -0
- package/src/scripts/review_axis_report.ts +218 -0
- package/src/scripts/routing_doctor.ts +5 -1
- package/src/scripts/run_supervise.ts +46 -1
- package/src/scripts/schemas/evals.schema.json +5 -0
- package/src/scripts/sync_pr_branch.ts +170 -20
- package/dist/cli/commands/commands.candidates.test.js +0 -257
- package/dist/cli/commands/commands.candidates.test.js.map +0 -1
- package/dist/cli/commands/profiles.test.js +0 -88
- package/dist/cli/commands/profiles.test.js.map +0 -1
- package/dist/cli/commands/recordTriggerEval.test.js +0 -113
- package/dist/cli/commands/recordTriggerEval.test.js.map +0 -1
- package/dist/cli/configRoot.test.js +0 -97
- package/dist/cli/configRoot.test.js.map +0 -1
- package/dist/cli/mcp/content.test.js +0 -54
- package/dist/cli/mcp/content.test.js.map +0 -1
- package/dist/cli/mcp/dispatch.test.js +0 -108
- package/dist/cli/mcp/dispatch.test.js.map +0 -1
- package/dist/server/writeRoot.test.js +0 -91
- package/dist/server/writeRoot.test.js.map +0 -1
- package/dist/shared/capabilities.test.js +0 -39
- package/dist/shared/capabilities.test.js.map +0 -1
- package/dist/ui/assets/index-C1pBRm1f.js +0 -26
- package/dist/ui/assets/index-C1pBRm1f.js.map +0 -1
- package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
- package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
- package/dist/cli-delegate/{chunk-GJ5BVHXM.js → chunk-PMEKGDED.js} +3 -3
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* ADR evidence census — a PROPOSAL-ONLY pre-grader for `provenance` + `evidence`.
|
|
4
|
+
*
|
|
5
|
+
* It reads every ADR record, greps for evidence markers, and emits a *proposed*
|
|
6
|
+
* `provenance.kind` and `evidence.strength` per record with the matched
|
|
7
|
+
* `file:line` for each marker. That is the whole output. It writes no ADR
|
|
8
|
+
* frontmatter, and it is not a gate.
|
|
9
|
+
*
|
|
10
|
+
* ## Why proposal-only, and why that constraint is the point
|
|
11
|
+
*
|
|
12
|
+
* `adr-layout § Provenance and evidence` closes with "a census may **propose** a
|
|
13
|
+
* value; it never writes one", and the `No bulk classification` clause exists
|
|
14
|
+
* because an agent grading its own homework unreviewed is the failure mode the
|
|
15
|
+
* corpus already paid for once: 44 engine-shaped REJECT records accumulated
|
|
16
|
+
* under a council-carried interpretation, and only a measurement disposed of
|
|
17
|
+
* the feature (`engine-reclassification-2026-07.md`;
|
|
18
|
+
* `claims:code-graph-retrieval-null`, recall 0.365 vs disciplined grep 0.797).
|
|
19
|
+
* A tool that both assigns a grade and lands it in the tree reproduces that
|
|
20
|
+
* failure with better metadata. So the output is one review artifact plus
|
|
21
|
+
* stdout, and a human moves any of it into a record.
|
|
22
|
+
*
|
|
23
|
+
* ## Two absolute rules, taken straight from the contract
|
|
24
|
+
*
|
|
25
|
+
* 1. **A council marker alone never raises the grade above E0.** Council
|
|
26
|
+
* consensus is not evidence — it is correlated agreement. Implemented
|
|
27
|
+
* structurally rather than as a special case: the grade is computed from the
|
|
28
|
+
* NON-council marker set only, so no amount of council signal can move it.
|
|
29
|
+
* 2. **Defaults are conservative.** `provenance: unknown`, `strength: E0`,
|
|
30
|
+
* `discovery: incomplete`.
|
|
31
|
+
*
|
|
32
|
+
* ## `discovery` is ALWAYS `incomplete`, by construction
|
|
33
|
+
*
|
|
34
|
+
* The contract permits `complete` only "until a defined evidence search has run
|
|
35
|
+
* and found nothing". This script runs a marker grep over the record's own text.
|
|
36
|
+
* That is not a defined exhaustive search of the tree, the claims ledger, the
|
|
37
|
+
* benchmark corpus, or the external sources a record might have cited
|
|
38
|
+
* elsewhere — so the honest value is `incomplete` for every record, at every
|
|
39
|
+
* grade, and this script has no code path that emits `complete`. If a future
|
|
40
|
+
* version wants `complete`, it must first state in code what exhaustive search
|
|
41
|
+
* justified it.
|
|
42
|
+
*
|
|
43
|
+
* ## `E4` is never proposed
|
|
44
|
+
*
|
|
45
|
+
* E4 is an external constraint — protocol compatibility with real consumers, a
|
|
46
|
+
* legal obligation, a demonstrated security invariant. None of those are
|
|
47
|
+
* decidable from marker presence: whether a consumer actually exists is a fact
|
|
48
|
+
* about the world, not about the text. The ceiling here is E3.
|
|
49
|
+
*
|
|
50
|
+
* ## The grades are heuristic — biased LOW, EXCEPT for two classes biased HIGH
|
|
51
|
+
*
|
|
52
|
+
* A marker is a proxy for a citation, not the citation. For most classes the
|
|
53
|
+
* bias direction was chosen deliberately downward: nothing acts on these numbers
|
|
54
|
+
* (no authority is derived from a grade — `adr-layout`, and the roadmap's
|
|
55
|
+
* Architecture table), so an under-graded proposal costs a reviewer one upgrade,
|
|
56
|
+
* while an over-graded one would put unearned "this is measured" text into the
|
|
57
|
+
* tree. `measurement` needs a figure or a dated verb; `repeated` needs something
|
|
58
|
+
* measured to compare; `prereg` needs a benchmark corpus beside it; `owner` and
|
|
59
|
+
* `council` raise nothing at all.
|
|
60
|
+
*
|
|
61
|
+
* **Two classes are biased HIGH by construction, and the sentence above is false
|
|
62
|
+
* for them.** `claim` and `external_standard` short-circuit to `E3` — the
|
|
63
|
+
* second-highest grade in the scale, and the highest this script proposes at
|
|
64
|
+
* all — on a bare textual match, ahead of every other rule and with no
|
|
65
|
+
* companion marker required. Both are the FIRST and THIRD branches of
|
|
66
|
+
* `proposeStrength`, so they pre-empt the measurement
|
|
67
|
+
* rules rather than competing with them. `EXTERNAL_STANDARD_RE` in particular
|
|
68
|
+
* matches a plain word-bounded `GDPR`, so naming a regulation as the SUBJECT of
|
|
69
|
+
* a decision reads identically to citing it as the evidence FOR one.
|
|
70
|
+
*
|
|
71
|
+
* Worked example, checkable against the corpus: `ADR-107` line 35 writes
|
|
72
|
+
* "EU DPA / GDPR Art. 28" to scope which legal domain its eval fixtures cover.
|
|
73
|
+
* The census proposes `E3 — cites a named external standard or vendor guidance`;
|
|
74
|
+
* the tranche reviewer reading the same line adjudicated `E2`. The grading rules
|
|
75
|
+
* are deliberately UNCHANGED — this paragraph exists so the artifact's own
|
|
76
|
+
* description of its bias is not contradicted by two of its own rules, and so a
|
|
77
|
+
* reviewer knows to check an `E3` sourced from `claim` or `external_standard`
|
|
78
|
+
* DOWNWARD rather than upward. Every proposal carries its matched lines so a
|
|
79
|
+
* reviewer checks the claim rather than the grade.
|
|
80
|
+
*
|
|
81
|
+
* Usage:
|
|
82
|
+
* ./scripts-run src/scripts/adr/evidence_census # write the artifact
|
|
83
|
+
* ./scripts-run src/scripts/adr/evidence_census --json # machine output
|
|
84
|
+
* ./scripts-run src/scripts/adr/evidence_census --check # scan-completeness only
|
|
85
|
+
* ./scripts-run src/scripts/adr/evidence_census --out <path>
|
|
86
|
+
*
|
|
87
|
+
* Exit codes: 0 ok · 1 the scan could not complete (an unreadable record, or an
|
|
88
|
+
* empty corpus) · 2 usage error. Never non-zero on a grade — a report is not a
|
|
89
|
+
* gate.
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
import * as fs from 'node:fs';
|
|
93
|
+
import * as path from 'node:path';
|
|
94
|
+
import process from 'node:process';
|
|
95
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
96
|
+
|
|
97
|
+
import { GateLedger } from '../_lib/gate_ledger.js';
|
|
98
|
+
import { assertScanned, DeadScopeError } from '../_lib/scan_scope.js';
|
|
99
|
+
import {
|
|
100
|
+
type AdrFrontmatter,
|
|
101
|
+
evidenceOf,
|
|
102
|
+
provenanceOf,
|
|
103
|
+
readAdrFrontmatter,
|
|
104
|
+
} from '../_lib/adr_frontmatter.js';
|
|
105
|
+
|
|
106
|
+
const _HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
107
|
+
export const REPO_ROOT = path.resolve(_HERE, '..', '..', '..');
|
|
108
|
+
const FLAT_DIR = path.join('docs', 'decisions');
|
|
109
|
+
const AREA_DIR = path.join('docs', 'adrs');
|
|
110
|
+
|
|
111
|
+
export const DEFAULT_OUT = path.join(
|
|
112
|
+
'agents',
|
|
113
|
+
'evidence',
|
|
114
|
+
'analysis',
|
|
115
|
+
'adr-evidence-census-2026-08.md',
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Marker taxonomy
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The marker classes. `council` is listed with the others and then excluded
|
|
124
|
+
* from grading — keeping it in the taxonomy is what makes "this record's only
|
|
125
|
+
* support is agreement" a *reportable* state rather than an absence.
|
|
126
|
+
*/
|
|
127
|
+
export type MarkerClass =
|
|
128
|
+
| 'claim'
|
|
129
|
+
| 'benchmark'
|
|
130
|
+
| 'prereg'
|
|
131
|
+
| 'measurement'
|
|
132
|
+
| 'repeated'
|
|
133
|
+
| 'external_standard'
|
|
134
|
+
| 'external_citation'
|
|
135
|
+
| 'council'
|
|
136
|
+
| 'owner';
|
|
137
|
+
|
|
138
|
+
/** Council markers carry provenance signal and never contribute to the grade. */
|
|
139
|
+
const COUNCIL_CLASSES: ReadonlySet<MarkerClass> = new Set<MarkerClass>(['council']);
|
|
140
|
+
|
|
141
|
+
export interface Marker {
|
|
142
|
+
cls: MarkerClass;
|
|
143
|
+
/** 1-based line number in the record. */
|
|
144
|
+
line: number;
|
|
145
|
+
/** The matched text, trimmed — evidence for the proposal. */
|
|
146
|
+
excerpt: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A `docs/CLAIMS.md` reference or a `claim:`/`claims:` id. */
|
|
150
|
+
const CLAIM_RE = /docs\/CLAIMS\.md(?:#[a-z0-9-]+)?|\bclaims?:\s?[a-z][a-z0-9-]{3,}/i;
|
|
151
|
+
|
|
152
|
+
/** A benchmark corpus or report path. */
|
|
153
|
+
const BENCHMARK_RE = /docs\/benchmark\.md|\binternal\/(?:bench|reports)\/[\w./-]*|\bPREREG[\w.-]*/;
|
|
154
|
+
|
|
155
|
+
/** Pre-registration language — the discipline, not the artifact. */
|
|
156
|
+
const PREREG_RE = /\bpre-?regist(?:er|ered|ration)\b|\bpre-?registered\b/i;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* A named external standard. Deliberately separate from a bare URL: the
|
|
160
|
+
* contract's E3 row names "established community standard" and "applicable
|
|
161
|
+
* vendor guidance", and a link is not by itself either of those.
|
|
162
|
+
*/
|
|
163
|
+
const EXTERNAL_STANDARD_RE =
|
|
164
|
+
/\bPSR-\d+\b|\bRFC\s?\d{3,4}\b|\bWCAG\b|\bISO[\s-]?\d{4,5}\b|\bOWASP\b|\bPHP-FIG\b|\bMADR\b|\bSemVer\b|\bDTCG\b|\bC2PA\b|\bGDPR\b|\bconforms to the [\w -]*standard\b/;
|
|
165
|
+
|
|
166
|
+
/** A bare external citation — real, weaker than a named standard. */
|
|
167
|
+
const EXTERNAL_CITATION_RE = /https?:\/\/\S+/;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Council / multi-model agreement, including bare model ids.
|
|
171
|
+
*
|
|
172
|
+
* The model-id patterns are family-scoped, not prefix-scoped. A bare
|
|
173
|
+
* `claude-[\w.-]+` matched `claude-code`, `.claude-plugin/marketplace.json`,
|
|
174
|
+
* `claude-desktop.md` and `claude-code-rules-dir-contract.md` — host names and
|
|
175
|
+
* file paths, none of which is a model that decided anything. Measured: it put
|
|
176
|
+
* 8 records into `agentic`/`mixed` on a filename.
|
|
177
|
+
*/
|
|
178
|
+
const COUNCIL_RE =
|
|
179
|
+
/\bAI council\b|\bcouncil (?:pass|passes|session|verdict|ruling|seat|seats|convergen\w*)\b|\bboth seats\b|\btwo-seat\b|\bconvergen\w+\b|\b(?:anthropic|openai|google)\/[\w.-]+|\bclaude-(?:sonnet|opus|haiku|fable|instant|\d)[\w.-]*|\bgpt-[\do][\w.-]*/i;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* An owner / maintainer / operator decision. Matched on a DECLARATION, never on
|
|
183
|
+
* the bare noun: the contract names "the word 'maintainer' appears in
|
|
184
|
+
* Consequences" as the false positive to avoid, so `maintainer` alone is not a
|
|
185
|
+
* marker and Consequences sections are excluded from this class entirely (see
|
|
186
|
+
* `sectionOf`).
|
|
187
|
+
*/
|
|
188
|
+
const OWNER_RE =
|
|
189
|
+
/\b(?:owner|maintainer|maintainer's|operator|operator's)\s+(?:decision|directive|instruction|ruling|requirement|intent|call|choice)\b|\bOwner decision\b|\bthe (?:owner|maintainer|operator) (?:decided|ruled|directed|instructed|requires|required)\b|\bowner-reserved ruling\b|\bauthority_basis:\s*owner_intent\b/i;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Repeated / comparative language. On its own this proves nothing, so E2
|
|
193
|
+
* additionally requires a measurement, benchmark, or claim marker — which is
|
|
194
|
+
* what the contract's E2 row actually says ("reproducible comparison, multiple
|
|
195
|
+
* independent incidents, bounded A/B"): the comparison has to be OF something.
|
|
196
|
+
*
|
|
197
|
+
* `sweep` is QUALIFIED rather than bare, and the qualifier is the whole point.
|
|
198
|
+
* In this tree the word covers two unrelated things: a parameter sweep (`docs/
|
|
199
|
+
* benchmark.md § Cost-factor sweep` — multiple measured points, genuinely
|
|
200
|
+
* comparative) and a corpus pass (`ADR-214`'s "package-wide consistency sweep",
|
|
201
|
+
* `ADR-216`'s `adoption-anchor sweep`), which compares nothing. Measured: a bare
|
|
202
|
+
* `\bsweep\b` put 7 records into E2 on a naming coincidence.
|
|
203
|
+
*/
|
|
204
|
+
const REPEATED_RE =
|
|
205
|
+
/\bmeasured (?:twice|three times|four times|\d+ times)\b|\bre-?measured\b|\breproducib\w+\b|\btwo independent\b|\bmultiple independent\b|\bA\/B\b|\bboth arms\b|\bacross (?:two|three|four|\d+) (?:runs|sessions|incidents|hosts|repos|repositories)\b|\bcomparative\b|\b(?:parameter|cost-factor|factor|benchmark|measurement|token|threshold|band)[\s-]sweep\b/i;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A digit that opens a QUANTITY rather than sitting inside an identifier.
|
|
209
|
+
*
|
|
210
|
+
* `\b` before `\d` matches inside one, so `Tier-3 host`, `P3.2 ADR` and
|
|
211
|
+
* `ADR-098` all read as figures ("3 host", "2 ADR"). Measured on the corpus:
|
|
212
|
+
* that shape put `ADR-001` and `ADR-071` at E1 on a tier label and a table cell.
|
|
213
|
+
*/
|
|
214
|
+
const NOT_IDENTIFIER = String.raw`(?<![\w.-])`;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* A percentage or a percentage-point figure.
|
|
218
|
+
*
|
|
219
|
+
* The `\b` sits INSIDE the alternation, on `pp` only, and that placement is the
|
|
220
|
+
* whole pattern. A trailing `\b` after `(?:%|pp)` made this regex dead code: `%`
|
|
221
|
+
* is a non-word character, so a boundary after it requires the NEXT character to
|
|
222
|
+
* be a word character — and a percentage is followed by a space, a `)`, or a
|
|
223
|
+
* `*` essentially always. `10 %`, `(179.1 %)` and `3.1 % of its allowance` all
|
|
224
|
+
* failed, which is why the corpus's percentage tables were being detected (when
|
|
225
|
+
* they were detected at all) through the unit and ratio patterns instead. `pp`
|
|
226
|
+
* keeps its boundary because `pp` is word characters and `450 ppm` is not a
|
|
227
|
+
* percentage-point figure.
|
|
228
|
+
*/
|
|
229
|
+
const PERCENT_RE = new RegExp(`${NOT_IDENTIFIER}\\d+(?:\\.\\d+)?\\s?(?:%|pp\\b)`);
|
|
230
|
+
|
|
231
|
+
/** A ratio or multiplier: `11.7x`, `0.365 vs 0.797`, `4 of 12`. */
|
|
232
|
+
const RATIO_RE = new RegExp(
|
|
233
|
+
[
|
|
234
|
+
`${NOT_IDENTIFIER}\\d+(?:\\.\\d+)?\\s?x\\b`,
|
|
235
|
+
`${NOT_IDENTIFIER}\\d+(?:\\.\\d+)?\\s+vs\\.?\\s+\\d+(?:\\.\\d+)?\\b`,
|
|
236
|
+
`${NOT_IDENTIFIER}\\d+\\s+of\\s+\\d+\\b`,
|
|
237
|
+
].join('|'),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* A figure with a counted unit. The unit list is closed on purpose: an open
|
|
242
|
+
* `\d+\s+\w+` matched version strings (`6.0.0-D`), section numbers, and dates,
|
|
243
|
+
* so every one of those became a "measurement".
|
|
244
|
+
*/
|
|
245
|
+
const UNIT_RE = new RegExp(
|
|
246
|
+
`${NOT_IDENTIFIER}(?:~|\u2248|>|<|\u2265|\u2264)?\\s?\\d[\\d,]*\\s+` +
|
|
247
|
+
'(?:commands?|rules?|skills?|ADRs?|records?|files?|lines?|chars?|characters?|tokens?|' +
|
|
248
|
+
'sessions?|runs?|entries|hosts?|seats?|jobs?|gates?|checks?|PRs?|consumers?|projects?|' +
|
|
249
|
+
'packages?|incidents?|minutes?|seconds?|hours?|days?|ms|MB|KB|GB|occurrences?|' +
|
|
250
|
+
'violations?|findings?)\\b',
|
|
251
|
+
'i',
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A measurement VERB, for a line that reports an observation without printing a
|
|
256
|
+
* figure. Needs a dated / PR'd / sha'd anchor nearby to count.
|
|
257
|
+
*
|
|
258
|
+
* Two exclusions, both from corpus false positives. `\b` before `measur` makes
|
|
259
|
+
* `unmeasured` a non-match — "the redundancy assumption is unmeasured"
|
|
260
|
+
* (`ADR-106:38`) is the ABSENCE of a measurement, and must never grade as one.
|
|
261
|
+
* `(?!-)` after the verb makes `measure-then-decide` and `measure-first` a
|
|
262
|
+
* non-match: those name a GATE that has not run yet, which is the same absence
|
|
263
|
+
* wearing a different suffix (`ADR-100:15`).
|
|
264
|
+
*/
|
|
265
|
+
const MEASURE_VERB_RE =
|
|
266
|
+
/\bmeasur(?:e|ed|es|ing|ement|ements)\b(?!-)|\bbenchmarked\b|\brecall\b|\bprecision\b|\blatency\b|\bthroughput\b|\bobserved\b|\bcounted\b/i;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The co-location anchors that date a bare measurement VERB: an ISO date, a PR
|
|
270
|
+
* number, or a commit sha / run id.
|
|
271
|
+
*/
|
|
272
|
+
const ANCHOR_RE = /\b\d{4}-\d{2}-\d{2}\b|\bPR\s?#?\d{3,5}\b|#\d{3,5}\b|\b[0-9a-f]{9,40}\b/;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* How many lines either side of a measurement verb may carry the date / PR /
|
|
276
|
+
* sha anchor.
|
|
277
|
+
*
|
|
278
|
+
* Two, not zero: real records put the provenance on the sentence's first line
|
|
279
|
+
* and the reference on its second (`ADR-229:52-53` — "**measured twice** on
|
|
280
|
+
* this repository (PR" then "#1277/#1280 and #1280/#1281"). A same-line
|
|
281
|
+
* requirement read that as undated.
|
|
282
|
+
*/
|
|
283
|
+
export const ANCHOR_WINDOW = 2;
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Detection
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Which `## Heading` a line sits under, lower-cased. Used only to exclude
|
|
291
|
+
* Consequences from the owner class — the contract's named false positive.
|
|
292
|
+
*/
|
|
293
|
+
export function sectionOf(lines: readonly string[], index: number): string {
|
|
294
|
+
for (let i = index; i >= 0; i -= 1) {
|
|
295
|
+
const line = lines[i];
|
|
296
|
+
if (line === undefined) continue;
|
|
297
|
+
const m = /^#{2,3}\s+(.*)$/.exec(line);
|
|
298
|
+
if (m?.[1] !== undefined) return m[1].trim().toLowerCase();
|
|
299
|
+
}
|
|
300
|
+
return '';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function anchoredNear(lines: readonly string[], index: number): boolean {
|
|
304
|
+
const from = Math.max(0, index - ANCHOR_WINDOW);
|
|
305
|
+
const to = Math.min(lines.length - 1, index + ANCHOR_WINDOW);
|
|
306
|
+
for (let i = from; i <= to; i += 1) {
|
|
307
|
+
const line = lines[i];
|
|
308
|
+
if (line !== undefined && ANCHOR_RE.test(line)) return true;
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function excerpt(line: string): string {
|
|
314
|
+
const t = line.trim();
|
|
315
|
+
return t.length <= 120 ? t : `${t.slice(0, 117)}\u2026`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The 0-based index of the line AFTER the closing `---`, or 0 when there is no
|
|
320
|
+
* frontmatter.
|
|
321
|
+
*
|
|
322
|
+
* Measurement detection starts here rather than at line 0, and that boundary
|
|
323
|
+
* fixed a false positive class: `decision: recursive-verification-benchmark-gate`
|
|
324
|
+
* (`ADR-106:5`) matched the benchmark verb, and the `date:` two lines above it
|
|
325
|
+
* supplied the anchor, so a SLUG graded as a dated measurement. A slug names the
|
|
326
|
+
* decision; it reports nothing. Every other marker class still scans the whole
|
|
327
|
+
* file, because a `basis:` list citing `docs/CLAIMS.md` and an
|
|
328
|
+
* `authority_basis: owner_intent` line are both real signal and both live in
|
|
329
|
+
* frontmatter.
|
|
330
|
+
*/
|
|
331
|
+
export function bodyStart(lines: readonly string[]): number {
|
|
332
|
+
if (lines[0] !== '---') return 0;
|
|
333
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
334
|
+
if (lines[i] === '---') return i + 1;
|
|
335
|
+
}
|
|
336
|
+
return 0;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Every evidence marker in one record, with its 1-based line number. */
|
|
340
|
+
export function detectMarkers(text: string): Marker[] {
|
|
341
|
+
const lines = text.split('\n');
|
|
342
|
+
const body = bodyStart(lines);
|
|
343
|
+
const out: Marker[] = [];
|
|
344
|
+
|
|
345
|
+
lines.forEach((line, index) => {
|
|
346
|
+
const push = (cls: MarkerClass): void => {
|
|
347
|
+
out.push({ cls, line: index + 1, excerpt: excerpt(line) });
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// Whole-file classes CITE a source or DECLARE an authority, and both
|
|
351
|
+
// legitimately appear in frontmatter — a `basis:` list pointing at
|
|
352
|
+
// `docs/CLAIMS.md`, an `authority_basis: owner_intent` line.
|
|
353
|
+
if (CLAIM_RE.test(line)) push('claim');
|
|
354
|
+
if (BENCHMARK_RE.test(line)) push('benchmark');
|
|
355
|
+
if (PREREG_RE.test(line)) push('prereg');
|
|
356
|
+
if (EXTERNAL_STANDARD_RE.test(line)) push('external_standard');
|
|
357
|
+
if (EXTERNAL_CITATION_RE.test(line)) push('external_citation');
|
|
358
|
+
if (OWNER_RE.test(line) && !sectionOf(lines, index).startsWith('consequence')) {
|
|
359
|
+
push('owner');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Body-only classes DESCRIBE how the work was done, and a slug or a
|
|
363
|
+
// `phase:` name is a label rather than a description. Measured:
|
|
364
|
+
// `ADR-216`'s `phase: … adoption-anchor sweep` (`:8`) matched the
|
|
365
|
+
// repeated/comparative class from frontmatter alone, which would have
|
|
366
|
+
// priced a naming coincidence as a comparison.
|
|
367
|
+
if (index < body) return;
|
|
368
|
+
if (COUNCIL_RE.test(line)) push('council');
|
|
369
|
+
if (REPEATED_RE.test(line)) push('repeated');
|
|
370
|
+
|
|
371
|
+
// A FIGURE — a percentage, a ratio, or a closed-list counted unit — is a
|
|
372
|
+
// quantified observation on its own. It does NOT need a date beside it:
|
|
373
|
+
// the record's own `date:` dates it, and requiring a second, adjacent
|
|
374
|
+
// date deleted real measurements rather than filtering noise. Measured
|
|
375
|
+
// on the corpus: `ADR-227` prints exact-BPE token counts against a
|
|
376
|
+
// 106,704 baseline (`:37`), a `179.1 %` figure (`:40`), `25 of 116`
|
|
377
|
+
// (`:66`) and a three-row percentage table (`:107-109`) — and every one
|
|
378
|
+
// of them was dropped, because that record's only dates are its
|
|
379
|
+
// frontmatter and its Status line. It graded E0. The literal
|
|
380
|
+
// co-location rule was the defect, so it now binds only the verb case
|
|
381
|
+
// below, where there is no figure to quantify anything.
|
|
382
|
+
//
|
|
383
|
+
// The noise the anchor was supposed to stop is stopped by the figure
|
|
384
|
+
// patterns instead: a percentage, an `Nx` / `N vs M` / `N of M` ratio,
|
|
385
|
+
// and a number followed by one of a CLOSED unit list. An open
|
|
386
|
+
// `\d+\s+\w+` matched version strings (`6.0.0-D`), phase numbers, and
|
|
387
|
+
// dates, which is why the list is closed rather than inferred.
|
|
388
|
+
if (PERCENT_RE.test(line) || RATIO_RE.test(line) || UNIT_RE.test(line)) {
|
|
389
|
+
push('measurement');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
// A verb with no figure reports an observation without printing one, so
|
|
393
|
+
// it still needs a date / PR / sha nearby to be a dated observation
|
|
394
|
+
// rather than the word "measured" in a sentence about measuring.
|
|
395
|
+
if (MEASURE_VERB_RE.test(line) && anchoredNear(lines, index)) push('measurement');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// Proposal
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
export interface Proposal {
|
|
406
|
+
provenance: string;
|
|
407
|
+
agenticMode: string | null;
|
|
408
|
+
strength: string;
|
|
409
|
+
discovery: 'incomplete';
|
|
410
|
+
rationale: string;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function classes(markers: readonly Marker[]): Set<MarkerClass> {
|
|
414
|
+
return new Set(markers.map((m) => m.cls));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Why a marker class that IS present still raised nothing.
|
|
419
|
+
*
|
|
420
|
+
* Four classes can be detected and grade to E0 on their own, because every rule
|
|
421
|
+
* that reads them requires a companion: `repeated` needs something measured to
|
|
422
|
+
* compare, `prereg` needs a benchmark corpus, `benchmark` needs a measurement or
|
|
423
|
+
* a pre-registration, and `owner` is a provenance input that the grading path
|
|
424
|
+
* never reads at all. Before this table existed those records printed
|
|
425
|
+
* "no evidence marker found" beside a Matched-markers cell that listed markers
|
|
426
|
+
* — 12 of 186 rows in the shipped artifact, `ADR-032` / `056` / `085` / `093` /
|
|
427
|
+
* `101` / `104` / `108` / `111` / `112` / `130` / `211` / `230`. The grade was
|
|
428
|
+
* right and the sentence was false, which is worse than a wrong grade: a
|
|
429
|
+
* reviewer who trusts the rationale stops reading the citation column.
|
|
430
|
+
*/
|
|
431
|
+
const UNGRADED_REASON: Readonly<Partial<Record<MarkerClass, string>>> = {
|
|
432
|
+
owner: 'an owner or maintainer declaration is provenance, not evidence',
|
|
433
|
+
repeated: 'comparative language with nothing measured to compare (E2 needs a measurement or a benchmark)',
|
|
434
|
+
prereg: 'pre-registration language with no benchmark corpus beside it (E3 needs both)',
|
|
435
|
+
benchmark: 'a benchmark path with no measurement or pre-registration beside it (E2 needs one)',
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* The ungraded classes, named, in first-appearance order — `classes()` builds
|
|
440
|
+
* its Set from the marker list, so the order is the record's own and is stable
|
|
441
|
+
* across runs.
|
|
442
|
+
*/
|
|
443
|
+
export function describeUngraded(present: ReadonlySet<MarkerClass>): string {
|
|
444
|
+
const parts = [...present]
|
|
445
|
+
.filter((cls) => !COUNCIL_CLASSES.has(cls))
|
|
446
|
+
.map((cls) => `${cls} (${UNGRADED_REASON[cls] ?? 'no rule raises the grade on this class alone'})`);
|
|
447
|
+
return parts.join('; ');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Propose `evidence.strength` from the NON-council marker set.
|
|
452
|
+
*
|
|
453
|
+
* The council exclusion is the structure, not a branch: `present` is built from
|
|
454
|
+
* a filtered list, so rule 1 ("a council marker alone never raises the grade
|
|
455
|
+
* above E0") cannot be defeated by adding another council pattern later.
|
|
456
|
+
*/
|
|
457
|
+
export function proposeStrength(markers: readonly Marker[]): { strength: string; rationale: string } {
|
|
458
|
+
const graded = markers.filter((m) => !COUNCIL_CLASSES.has(m.cls));
|
|
459
|
+
const present = classes(graded);
|
|
460
|
+
const councilOnly = graded.length === 0 && markers.length > 0;
|
|
461
|
+
|
|
462
|
+
if (present.has('claim')) {
|
|
463
|
+
return { strength: 'E3', rationale: 'cites a CLAIMS.md claim id (pre-registered and hash-bound in this repo)' };
|
|
464
|
+
}
|
|
465
|
+
if (present.has('prereg') && present.has('benchmark')) {
|
|
466
|
+
return { strength: 'E3', rationale: 'pre-registered benchmark' };
|
|
467
|
+
}
|
|
468
|
+
if (present.has('external_standard')) {
|
|
469
|
+
return { strength: 'E3', rationale: 'cites a named external standard or vendor guidance' };
|
|
470
|
+
}
|
|
471
|
+
if (present.has('repeated') && (present.has('measurement') || present.has('benchmark'))) {
|
|
472
|
+
return { strength: 'E2', rationale: 'repeated or comparative measurement' };
|
|
473
|
+
}
|
|
474
|
+
if (present.has('measurement') && (present.has('benchmark') || present.has('prereg'))) {
|
|
475
|
+
return { strength: 'E2', rationale: 'measurement against a benchmark or pre-registered threshold' };
|
|
476
|
+
}
|
|
477
|
+
if (present.has('measurement')) {
|
|
478
|
+
return { strength: 'E1', rationale: 'one dated local observation' };
|
|
479
|
+
}
|
|
480
|
+
if (present.has('external_citation')) {
|
|
481
|
+
return { strength: 'E1', rationale: 'one external citation, no named standard' };
|
|
482
|
+
}
|
|
483
|
+
if (councilOnly) {
|
|
484
|
+
return {
|
|
485
|
+
strength: 'E0',
|
|
486
|
+
rationale: 'council / agreement markers only — consensus is not evidence',
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
// Markers WERE found and none of them is graded by any rule above. This
|
|
490
|
+
// branch exists because the fall-through used to claim the opposite; see
|
|
491
|
+
// `UNGRADED_REASON`.
|
|
492
|
+
if (graded.length > 0) {
|
|
493
|
+
return {
|
|
494
|
+
strength: 'E0',
|
|
495
|
+
rationale: `markers present, none graded: ${describeUngraded(present)}`,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
return { strength: 'E0', rationale: 'no evidence marker found' };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Propose `provenance.kind`.
|
|
503
|
+
*
|
|
504
|
+
* Owner and council markers are the only two inputs, and `reopen_policy: owner`
|
|
505
|
+
* is deliberately NOT one of them: the contract's own Iron Law says an ADR's
|
|
506
|
+
* historical decision-maker does not determine its reopen venue, and reading
|
|
507
|
+
* that backwards — venue implies author — is the same conflation in the other
|
|
508
|
+
* direction.
|
|
509
|
+
*/
|
|
510
|
+
export function proposeProvenance(markers: readonly Marker[]): {
|
|
511
|
+
provenance: string;
|
|
512
|
+
agenticMode: string | null;
|
|
513
|
+
} {
|
|
514
|
+
const present = classes(markers);
|
|
515
|
+
const human = present.has('owner');
|
|
516
|
+
const agent = present.has('council');
|
|
517
|
+
const mode = agent ? 'council' : null;
|
|
518
|
+
|
|
519
|
+
if (human && agent) return { provenance: 'mixed', agenticMode: mode };
|
|
520
|
+
if (human) return { provenance: 'human', agenticMode: null };
|
|
521
|
+
if (agent) return { provenance: 'agentic', agenticMode: mode };
|
|
522
|
+
return { provenance: 'unknown', agenticMode: null };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export function propose(markers: readonly Marker[]): Proposal {
|
|
526
|
+
const { strength, rationale } = proposeStrength(markers);
|
|
527
|
+
const { provenance, agenticMode } = proposeProvenance(markers);
|
|
528
|
+
return { provenance, agenticMode, strength, discovery: 'incomplete', rationale };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
// Corpus
|
|
533
|
+
// ---------------------------------------------------------------------------
|
|
534
|
+
|
|
535
|
+
export interface CensusRecord {
|
|
536
|
+
/** Repo-relative path. */
|
|
537
|
+
record: string;
|
|
538
|
+
surface: 'flat' | 'area';
|
|
539
|
+
/** `present`, or `absent` with what stands in for it. */
|
|
540
|
+
frontmatter: 'present' | 'absent';
|
|
541
|
+
/** What the record carries today on the two axes, for the diff column. */
|
|
542
|
+
current: string;
|
|
543
|
+
proposed: Proposal;
|
|
544
|
+
markers: Marker[];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export interface CensusResult {
|
|
548
|
+
scanned: number;
|
|
549
|
+
records: CensusRecord[];
|
|
550
|
+
/** Records that could not be read at all — the only `--check` failure. */
|
|
551
|
+
unreadable: { record: string; error: string }[];
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Enumerate the corpus: 177 flat `ADR-*.md` plus the per-area `NNNN-*.md`. */
|
|
555
|
+
export function enumerateRecords(root: string = REPO_ROOT): string[] {
|
|
556
|
+
const out: string[] = [];
|
|
557
|
+
|
|
558
|
+
const flat = path.join(root, FLAT_DIR);
|
|
559
|
+
if (fs.existsSync(flat)) {
|
|
560
|
+
for (const name of fs.readdirSync(flat).sort()) {
|
|
561
|
+
if (/^ADR-.*\.md$/.test(name)) out.push(path.join(FLAT_DIR, name));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const areas = path.join(root, AREA_DIR);
|
|
566
|
+
if (fs.existsSync(areas)) {
|
|
567
|
+
const walk = (dir: string, rel: string): void => {
|
|
568
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
569
|
+
const abs = path.join(dir, entry.name);
|
|
570
|
+
const next = path.join(rel, entry.name);
|
|
571
|
+
if (entry.isDirectory()) {
|
|
572
|
+
walk(abs, next);
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
// Every `README.md` is excluded — an area index is not a record.
|
|
576
|
+
if (entry.name === 'README.md') continue;
|
|
577
|
+
if (/^\d+.*\.md$/.test(entry.name)) out.push(next);
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
walk(areas, AREA_DIR);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return out;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* The per-area metadata line, which is a blockquote rather than frontmatter:
|
|
588
|
+
* `> Area: \`cost\` · Status: accepted · Date: 2026-05-16 · Type: retrospective`.
|
|
589
|
+
*
|
|
590
|
+
* Read for the report's "current" column only. A sibling change is converting
|
|
591
|
+
* these to real frontmatter; until it lands, reporting the blockquote is more
|
|
592
|
+
* useful than reporting nothing, and it is labelled as blockquote-derived so
|
|
593
|
+
* nobody mistakes it for a parsed axis.
|
|
594
|
+
*/
|
|
595
|
+
export function blockquoteMeta(text: string): string | null {
|
|
596
|
+
for (const line of text.split('\n').slice(0, 12)) {
|
|
597
|
+
if (/^>\s*Area:/.test(line)) return line.replace(/^>\s*/, '').trim();
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Render what the record carries on the two axes today. */
|
|
603
|
+
function currentAxes(fm: AdrFrontmatter | null, text: string): string {
|
|
604
|
+
if (fm === null) {
|
|
605
|
+
const meta = blockquoteMeta(text);
|
|
606
|
+
return meta === null
|
|
607
|
+
? 'no frontmatter, no blockquote metadata'
|
|
608
|
+
: `no frontmatter; blockquote: ${meta}`;
|
|
609
|
+
}
|
|
610
|
+
const prov = provenanceOf(fm);
|
|
611
|
+
const ev = evidenceOf(fm);
|
|
612
|
+
const parts: string[] = [];
|
|
613
|
+
parts.push(prov?.kind === undefined || prov?.kind === null ? 'provenance: —' : `provenance: ${prov.kind}`);
|
|
614
|
+
parts.push(ev?.strength === undefined || ev?.strength === null ? 'evidence: —' : `evidence: ${ev.strength}`);
|
|
615
|
+
const basis = fm.scalars['authority_basis'];
|
|
616
|
+
if (basis !== undefined && basis !== '') parts.push(`authority_basis: ${basis}`);
|
|
617
|
+
return parts.join(' · ');
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export function runCensus(root: string = REPO_ROOT, ledger?: GateLedger): CensusResult {
|
|
621
|
+
const rels = enumerateRecords(root);
|
|
622
|
+
ledger?.plan(rels);
|
|
623
|
+
|
|
624
|
+
const records: CensusRecord[] = [];
|
|
625
|
+
const unreadable: { record: string; error: string }[] = [];
|
|
626
|
+
|
|
627
|
+
for (const rel of rels) {
|
|
628
|
+
let text: string;
|
|
629
|
+
try {
|
|
630
|
+
text = fs.readFileSync(path.join(root, rel), 'utf-8');
|
|
631
|
+
} catch (exc) {
|
|
632
|
+
unreadable.push({ record: rel, error: exc instanceof Error ? exc.message : String(exc) });
|
|
633
|
+
ledger?.fail(rel, 'unreadable');
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
const fm = readAdrFrontmatter(text);
|
|
637
|
+
const markers = detectMarkers(text);
|
|
638
|
+
records.push({
|
|
639
|
+
record: rel,
|
|
640
|
+
surface: rel.startsWith(FLAT_DIR) ? 'flat' : 'area',
|
|
641
|
+
frontmatter: fm === null ? 'absent' : 'present',
|
|
642
|
+
current: currentAxes(fm, text),
|
|
643
|
+
proposed: propose(markers),
|
|
644
|
+
markers,
|
|
645
|
+
});
|
|
646
|
+
ledger?.complete(rel);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return { scanned: records.length, records, unreadable };
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ---------------------------------------------------------------------------
|
|
653
|
+
// Report
|
|
654
|
+
// ---------------------------------------------------------------------------
|
|
655
|
+
|
|
656
|
+
export function distribution(result: CensusResult): {
|
|
657
|
+
strength: Record<string, number>;
|
|
658
|
+
provenance: Record<string, number>;
|
|
659
|
+
frontmatter_absent: number;
|
|
660
|
+
} {
|
|
661
|
+
const strength: Record<string, number> = {};
|
|
662
|
+
const provenance: Record<string, number> = {};
|
|
663
|
+
let absent = 0;
|
|
664
|
+
for (const r of result.records) {
|
|
665
|
+
strength[r.proposed.strength] = (strength[r.proposed.strength] ?? 0) + 1;
|
|
666
|
+
provenance[r.proposed.provenance] = (provenance[r.proposed.provenance] ?? 0) + 1;
|
|
667
|
+
if (r.frontmatter === 'absent') absent += 1;
|
|
668
|
+
}
|
|
669
|
+
return { strength, provenance, frontmatter_absent: absent };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** `docs/decisions/ADR-048-x.md:30 council` — the citation, deduped per class. */
|
|
673
|
+
function renderMarkers(markers: readonly Marker[], record: string): string {
|
|
674
|
+
if (markers.length === 0) return '_none_';
|
|
675
|
+
const byClass = new Map<MarkerClass, number[]>();
|
|
676
|
+
for (const m of markers) {
|
|
677
|
+
const seen = byClass.get(m.cls) ?? [];
|
|
678
|
+
if (!seen.includes(m.line)) seen.push(m.line);
|
|
679
|
+
byClass.set(m.cls, seen);
|
|
680
|
+
}
|
|
681
|
+
const base = path.basename(record);
|
|
682
|
+
return [...byClass.entries()]
|
|
683
|
+
.map(([cls, lines]) => `${cls} @ ${lines.slice(0, 6).map((l) => `${base}:${String(l)}`).join(', ')}`)
|
|
684
|
+
.join('<br>');
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export function renderArtifact(result: CensusResult): string {
|
|
688
|
+
const dist = distribution(result);
|
|
689
|
+
const L: string[] = [];
|
|
690
|
+
|
|
691
|
+
// `analysis` per `docs/contracts/evidence-artifact-types.md`: "a
|
|
692
|
+
// measurement, census, investigation, or report … never re-bound". Emitted
|
|
693
|
+
// as line 1 because `lint_evidence_artifacts` fires on any artifact ADDED
|
|
694
|
+
// under `agents/evidence/` that resolves no type, and it reads only the
|
|
695
|
+
// first 40 lines. A generator that writes an undeclared artifact reds that
|
|
696
|
+
// gate on the commit, not on the run — which is exactly the class of defect
|
|
697
|
+
// that is cheap here and expensive later.
|
|
698
|
+
L.push('<!-- evidence-type: analysis -->');
|
|
699
|
+
L.push('');
|
|
700
|
+
L.push('# ADR evidence census — proposed provenance and evidence grades');
|
|
701
|
+
L.push('');
|
|
702
|
+
L.push(
|
|
703
|
+
`Generated by \`src/scripts/adr/evidence_census.ts\`. Records examined: **${String(result.scanned)}** ` +
|
|
704
|
+
`(${String(result.records.filter((r) => r.surface === 'flat').length)} flat, ` +
|
|
705
|
+
`${String(result.records.filter((r) => r.surface === 'area').length)} per-area).`,
|
|
706
|
+
);
|
|
707
|
+
L.push('');
|
|
708
|
+
L.push('## What this artifact is NOT');
|
|
709
|
+
L.push('');
|
|
710
|
+
L.push(
|
|
711
|
+
'It is a **proposal set for human review**. It wrote no ADR frontmatter, and it has no code ' +
|
|
712
|
+
'path that can: `adr-layout § Provenance and evidence` says a census may propose a value and ' +
|
|
713
|
+
'never writes one, and the `No bulk classification` clause is why. The grades are **heuristic** ' +
|
|
714
|
+
'— a marker grep over each record\'s own text, not a citation audit.',
|
|
715
|
+
);
|
|
716
|
+
L.push('');
|
|
717
|
+
L.push('### Which way each class is biased');
|
|
718
|
+
L.push('');
|
|
719
|
+
L.push(
|
|
720
|
+
'Most classes are biased **LOW** on purpose: an under-graded proposal costs a reviewer one ' +
|
|
721
|
+
'upgrade, while an over-graded one puts unearned "this is measured" text into the tree. ' +
|
|
722
|
+
'`measurement` needs a figure or a dated verb, `repeated` needs something measured to compare, ' +
|
|
723
|
+
'`prereg` needs a benchmark corpus beside it, and `owner` / `council` raise nothing at all.',
|
|
724
|
+
);
|
|
725
|
+
L.push('');
|
|
726
|
+
L.push(
|
|
727
|
+
'**Two classes are biased HIGH by construction, and the paragraph above is false for them.** ' +
|
|
728
|
+
'`claim` and `external_standard` short-circuit to `E3` — the second-highest grade in the ' +
|
|
729
|
+
'scale, and the highest this script proposes at all — on a bare textual match, ahead of ' +
|
|
730
|
+
'every other rule and with no companion marker ' +
|
|
731
|
+
'required: they are the first and third branches of `proposeStrength`, so they pre-empt the ' +
|
|
732
|
+
'measurement rules rather than competing with them. The standard pattern matches a plain ' +
|
|
733
|
+
'word-bounded `GDPR`, so naming a regulation as the *subject* of a decision reads identically ' +
|
|
734
|
+
'to citing it as the *evidence for* one.',
|
|
735
|
+
);
|
|
736
|
+
L.push('');
|
|
737
|
+
L.push(
|
|
738
|
+
'Worked example, checkable in this table: `ADR-107` line 35 writes "EU DPA / GDPR Art. 28" to ' +
|
|
739
|
+
'scope which legal domain its eval fixtures cover. This census proposes ' +
|
|
740
|
+
'`E3 — cites a named external standard or vendor guidance`; the tranche reviewer reading the ' +
|
|
741
|
+
'same line adjudicated `E2`. The grading rules are deliberately unchanged — so read an `E3` ' +
|
|
742
|
+
'whose only non-council marker is `claim` or `external_standard` as a **ceiling to check ' +
|
|
743
|
+
'downward**, not as a floor.',
|
|
744
|
+
);
|
|
745
|
+
L.push('');
|
|
746
|
+
L.push(
|
|
747
|
+
'`discovery` is `incomplete` for every row, at every grade, and this script cannot emit ' +
|
|
748
|
+
'`complete`. A marker grep is not the defined exhaustive evidence search the contract requires ' +
|
|
749
|
+
'before absence may be asserted. **E4 is never proposed** — an external constraint is a fact ' +
|
|
750
|
+
'about real consumers, contracts, or law, and marker presence cannot establish one.',
|
|
751
|
+
);
|
|
752
|
+
L.push('');
|
|
753
|
+
L.push(
|
|
754
|
+
'A council marker never raises a grade above `E0`. That is structural here, not a special case: ' +
|
|
755
|
+
'the grade is computed from the non-council marker set, so no future council pattern can move it.',
|
|
756
|
+
);
|
|
757
|
+
L.push('');
|
|
758
|
+
L.push('## Proposed distribution');
|
|
759
|
+
L.push('');
|
|
760
|
+
L.push('| Axis | Value | Records |');
|
|
761
|
+
L.push('|---|---|---|');
|
|
762
|
+
for (const key of ['E0', 'E1', 'E2', 'E3', 'E4']) {
|
|
763
|
+
L.push(`| \`evidence.strength\` | \`${key}\` | ${String(dist.strength[key] ?? 0)} |`);
|
|
764
|
+
}
|
|
765
|
+
for (const key of ['human', 'agentic', 'mixed', 'unknown']) {
|
|
766
|
+
L.push(`| \`provenance.kind\` | \`${key}\` | ${String(dist.provenance[key] ?? 0)} |`);
|
|
767
|
+
}
|
|
768
|
+
L.push(`| frontmatter | absent | ${String(dist.frontmatter_absent)} |`);
|
|
769
|
+
L.push('');
|
|
770
|
+
|
|
771
|
+
if (result.unreadable.length > 0) {
|
|
772
|
+
L.push('## Unreadable records');
|
|
773
|
+
L.push('');
|
|
774
|
+
for (const u of result.unreadable) L.push(`- \`${u.record}\` — ${u.error}`);
|
|
775
|
+
L.push('');
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
L.push('## Per-record proposals');
|
|
779
|
+
L.push('');
|
|
780
|
+
L.push(
|
|
781
|
+
'| Record | Proposed provenance | Proposed strength | Discovery | Matched markers (file:line) | Current frontmatter |',
|
|
782
|
+
);
|
|
783
|
+
L.push('|---|---|---|---|---|---|');
|
|
784
|
+
for (const r of result.records) {
|
|
785
|
+
const prov =
|
|
786
|
+
r.proposed.agenticMode === null
|
|
787
|
+
? `\`${r.proposed.provenance}\``
|
|
788
|
+
: `\`${r.proposed.provenance}\` (\`agentic_mode: ${r.proposed.agenticMode}\`)`;
|
|
789
|
+
L.push(
|
|
790
|
+
`| \`${r.record}\` | ${prov} | \`${r.proposed.strength}\` — ${r.proposed.rationale} | ` +
|
|
791
|
+
`\`${r.proposed.discovery}\` | ${renderMarkers(r.markers, r.record)} | ${r.current} |`,
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
L.push('');
|
|
795
|
+
return L.join('\n');
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// ---------------------------------------------------------------------------
|
|
799
|
+
// CLI
|
|
800
|
+
// ---------------------------------------------------------------------------
|
|
801
|
+
|
|
802
|
+
function main(argv: string[]): number {
|
|
803
|
+
const asJson = argv.includes('--json');
|
|
804
|
+
const checkOnly = argv.includes('--check');
|
|
805
|
+
const outIndex = argv.indexOf('--out');
|
|
806
|
+
if (outIndex !== -1 && argv[outIndex + 1] === undefined) {
|
|
807
|
+
process.stderr.write('usage: evidence_census [--json] [--check] [--out <path>]\n');
|
|
808
|
+
return 2;
|
|
809
|
+
}
|
|
810
|
+
const outRel = outIndex === -1 ? DEFAULT_OUT : (argv[outIndex + 1] as string);
|
|
811
|
+
|
|
812
|
+
const ledger = new GateLedger('adr_evidence_census');
|
|
813
|
+
const result = runCensus(REPO_ROOT, ledger);
|
|
814
|
+
const tally = ledger.finalize();
|
|
815
|
+
|
|
816
|
+
try {
|
|
817
|
+
assertScanned({
|
|
818
|
+
gate: 'adr_evidence_census',
|
|
819
|
+
scanned: result.scanned,
|
|
820
|
+
units: 'ADR record(s)',
|
|
821
|
+
roots: [FLAT_DIR, AREA_DIR],
|
|
822
|
+
});
|
|
823
|
+
} catch (exc) {
|
|
824
|
+
if (exc instanceof DeadScopeError) {
|
|
825
|
+
process.stderr.write(`❌ ${exc.message}\n`);
|
|
826
|
+
return 1;
|
|
827
|
+
}
|
|
828
|
+
throw exc;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (asJson) {
|
|
832
|
+
process.stdout.write(
|
|
833
|
+
JSON.stringify(
|
|
834
|
+
{
|
|
835
|
+
scanned: result.scanned,
|
|
836
|
+
distribution: distribution(result),
|
|
837
|
+
unreadable: result.unreadable,
|
|
838
|
+
records: result.records,
|
|
839
|
+
ledger: tally,
|
|
840
|
+
},
|
|
841
|
+
null,
|
|
842
|
+
2,
|
|
843
|
+
) + '\n',
|
|
844
|
+
);
|
|
845
|
+
return result.unreadable.length > 0 ? 1 : 0;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const dist = distribution(result);
|
|
849
|
+
// `path.join(REPO_ROOT, outRel)` silently rewrote an ABSOLUTE `--out` into
|
|
850
|
+
// the repo — `--out /tmp/x.md` wrote `<repo>/tmp/x.md` and then printed
|
|
851
|
+
// `/tmp/x.md`, so the caller was told a path that held nothing. Resolve
|
|
852
|
+
// instead, and print what was actually written. Found in completion review.
|
|
853
|
+
const abs = path.isAbsolute(outRel) ? outRel : path.join(REPO_ROOT, outRel);
|
|
854
|
+
if (!checkOnly) {
|
|
855
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
856
|
+
fs.writeFileSync(abs, renderArtifact(result), 'utf-8');
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
process.stdout.write(`scanned: ${String(result.scanned)}\n`);
|
|
860
|
+
process.stdout.write(
|
|
861
|
+
`ADR evidence census: ` +
|
|
862
|
+
(['E0', 'E1', 'E2', 'E3', 'E4'].map((k) => `${k}=${String(dist.strength[k] ?? 0)}`).join(' ')) +
|
|
863
|
+
` · ` +
|
|
864
|
+
(['human', 'agentic', 'mixed', 'unknown'].map((k) => `${k}=${String(dist.provenance[k] ?? 0)}`).join(' ')) +
|
|
865
|
+
`\n`,
|
|
866
|
+
);
|
|
867
|
+
ledger.report();
|
|
868
|
+
if (!checkOnly) process.stdout.write(`proposal artifact: ${path.relative(REPO_ROOT, abs) || abs}\n`);
|
|
869
|
+
|
|
870
|
+
if (result.unreadable.length > 0) {
|
|
871
|
+
for (const u of result.unreadable) {
|
|
872
|
+
process.stderr.write(` ❌ ${u.record}: ${u.error}\n`);
|
|
873
|
+
}
|
|
874
|
+
process.stderr.write(
|
|
875
|
+
`\n❌ adr_evidence_census: ${String(result.unreadable.length)} record(s) could not be read — the scan is incomplete.\n`,
|
|
876
|
+
);
|
|
877
|
+
return 1;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Deliberately never non-zero on a grade. This is a report; the proposals
|
|
881
|
+
// are reviewed by a human, and a low grade is a finding, not a violation.
|
|
882
|
+
process.stdout.write('✅ adr_evidence_census: scan complete (proposals only — nothing was written to any ADR)\n');
|
|
883
|
+
return 0;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (process.argv[1] !== undefined) {
|
|
887
|
+
try {
|
|
888
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
889
|
+
const argv1 = fs.realpathSync(path.resolve(process.argv[1]));
|
|
890
|
+
if (here === argv1) {
|
|
891
|
+
process.exit(main(process.argv.slice(2)));
|
|
892
|
+
}
|
|
893
|
+
} catch {
|
|
894
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
895
|
+
if (import.meta.url === argvUrl) {
|
|
896
|
+
process.exit(main(process.argv.slice(2)));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|