@event4u/agent-config 12.1.0 → 14.0.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 +164 -186
- package/README.md +1 -1
- package/dist/agent-src/commands/cost/report.md +23 -0
- package/dist/agent-src/commands/evals/roadmap.json +8 -0
- package/dist/agent-src/commands/fix/ci.md +39 -1
- package/dist/agent-src/commands/pr/create.md +65 -4
- package/dist/agent-src/commands/roadmap/next.md +28 -5
- package/dist/agent-src/commands/work.md +16 -0
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
- package/dist/agent-src/contexts/subagent-configuration.md +69 -7
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
- package/dist/agent-src/rules/design-fidelity.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +36 -14
- package/dist/agent-src/rules/evaluator-independence.md +27 -5
- package/dist/agent-src/rules/git-history-discipline.md +5 -1
- package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
- package/dist/agent-src/rules/ui-audit-gate.md +34 -12
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/dist/agent-src/scripts/gate_execute.ts +395 -0
- package/dist/agent-src/scripts/resume_probe.ts +422 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
- package/dist/agent-src/scripts/update_roadmap_progress.ts +70 -4
- package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
- package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
- package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
- package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
- package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
- package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
- package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
- package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +5 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
- package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +43 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-K34QTSD5.js → chunk-7PZ4FSZB.js} +368 -191
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-F3YUF7SA.js} +1 -1
- package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
- package/dist/cli-delegate/cmd_conformance.js +224 -104
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +73 -30
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -5
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +2653 -1365
- package/dist/install/install.mjs +12 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2679 -887
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +1 -1
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +2 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
- package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/design-fidelity-mechanics.md +21 -0
- package/docs/guidelines/design-handover-extraction.md +87 -0
- package/docs/guidelines/prompt-templates.md +24 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/src/agent-src/scripts/gate_execute.ts +395 -0
- package/src/agent-src/scripts/resume_probe.ts +422 -0
- package/src/agent-src/scripts/roadmap_gates.ts +138 -4
- package/src/agent-src/scripts/update_roadmap_progress.ts +70 -4
- package/src/config/ci-local-parity.yml +13 -0
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-coverage.yml +140 -12
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/hook-token-budget.json +8 -1
- package/src/config/rule-activation-census.json +41 -0
- package/src/config/rule-stub-ceilings.json +52 -0
- package/src/scripts/_dispatch.bash +4 -0
- package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
- package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
- package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
- package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
- package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
- package/src/scripts/_lib/evaluator_contract.ts +159 -0
- package/src/scripts/_lib/gh_transient.ts +80 -0
- package/src/scripts/_lib/release_material.ts +16 -5
- package/src/scripts/_lib/review_independence.ts +111 -0
- package/src/scripts/_lib/session_register.ts +14 -0
- package/src/scripts/_lib/skill_catalogue.ts +278 -5
- package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- package/src/scripts/_lib/turn_end_refusals.ts +482 -0
- package/src/scripts/ai_council/_py_json.ts +96 -0
- package/src/scripts/ai_council/cli_call_budget.ts +357 -0
- package/src/scripts/ai_council/clients.ts +101 -164
- package/src/scripts/ai_council/config.ts +25 -11
- package/src/scripts/ai_council/probe_store.ts +196 -0
- package/src/scripts/ai_council/qualification.ts +425 -0
- package/src/scripts/ai_council/qualification_wiring.ts +243 -0
- package/src/scripts/ai_council/quorum_wiring.ts +211 -0
- package/src/scripts/ai_team/team_dispatch.ts +40 -6
- package/src/scripts/bench_ab_v2_complexity.ts +252 -0
- package/src/scripts/bench_ab_v2_run.ts +3 -42
- package/src/scripts/bench_ab_v2_safety.ts +296 -0
- package/src/scripts/bench_ab_v2_search.ts +343 -0
- package/src/scripts/bench_ab_v2_stats.ts +191 -0
- package/src/scripts/build_archive_index.ts +383 -0
- package/src/scripts/capture_skill_catalogue.ts +369 -4
- package/src/scripts/check_evaluator_schema.ts +249 -0
- package/src/scripts/check_pr_ci_current.ts +409 -0
- package/src/scripts/check_review_schema.ts +195 -0
- package/src/scripts/check_rule_activation_census.ts +451 -0
- package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
- package/src/scripts/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -1
- package/src/scripts/condense.ts +1 -1
- package/src/scripts/cost/backfill_rates.mjs +298 -0
- package/src/scripts/cost/budget.mjs +12 -32
- package/src/scripts/cost_summary.ts +25 -0
- package/src/scripts/council_cli.ts +141 -191
- package/src/scripts/dispatch_r2_reviewer.ts +8 -1
- package/src/scripts/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +99 -6
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/dispatch_hook.ts +43 -1
- package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
- package/src/scripts/hooks/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/hooks_doctor.ts +112 -0
- package/src/scripts/interruption_report.ts +443 -0
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_evidence_artifacts.ts +359 -0
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_roadmap_blockers.ts +65 -0
- package/src/scripts/lint_roadmap_complexity.ts +7 -1
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_skill_router_head.ts +21 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
- package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
- package/src/scripts/mcp_server/tools.ts +87 -0
- package/src/scripts/release.ts +43 -58
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/rule_activation_census.ts +292 -0
- package/src/scripts/schemas/command.schema.json +1 -1
- package/src/scripts/schemas/evaluator-output.schema.json +60 -0
- package/src/scripts/schemas/review-findings.schema.json +31 -0
- package/src/scripts/schemas/skill.schema.json +18 -1
- package/src/scripts/self_review_gate.ts +6 -0
- package/src/scripts/session_register_hook.ts +30 -1
- package/src/scripts/sync_pr_branch.ts +271 -0
- package/src/scripts/test_council_qualification.ts +213 -0
- package/src/scripts/trigger_coverage.ts +224 -3
- package/src/server/schemas/settings.ts +2 -2
- package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
- package/src/config/budget-routing.json +0 -7
|
@@ -29,6 +29,7 @@ import * as fs from 'node:fs';
|
|
|
29
29
|
import * as path from 'node:path';
|
|
30
30
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
31
|
|
|
32
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
32
33
|
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
33
34
|
|
|
34
35
|
const QUIET = process.argv.includes('--quiet');
|
|
@@ -44,6 +45,14 @@ export type Status = (typeof STATUSES)[number];
|
|
|
44
45
|
export interface ParityFinding {
|
|
45
46
|
kind: string;
|
|
46
47
|
msg: string;
|
|
48
|
+
/**
|
|
49
|
+
* The catalog id this finding is about, when it is about one.
|
|
50
|
+
*
|
|
51
|
+
* Carried explicitly so the per-target ledger can attribute a failure
|
|
52
|
+
* without parsing it back out of `msg`. Absent on structural findings
|
|
53
|
+
* (a missing § Detector status section is about the document, not an id).
|
|
54
|
+
*/
|
|
55
|
+
id?: string;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
const ENTRY_ROW = /^\|\s*((?:V|C|T|L|M|CP)\d+)\s*\|/gm;
|
|
@@ -89,7 +98,7 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
|
|
|
89
98
|
const entries = catalogEntries(catalogMd);
|
|
90
99
|
const seen = new Set<string>();
|
|
91
100
|
for (const id of entries) {
|
|
92
|
-
if (seen.has(id)) out.push({ kind: 'duplicate-entry', msg: `${id}: appears more than once in the catalog tables` });
|
|
101
|
+
if (seen.has(id)) out.push({ kind: 'duplicate-entry', id, msg: `${id}: appears more than once in the catalog tables` });
|
|
93
102
|
seen.add(id);
|
|
94
103
|
}
|
|
95
104
|
|
|
@@ -98,13 +107,13 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
|
|
|
98
107
|
// A — every catalog entry is classified.
|
|
99
108
|
for (const id of seen) {
|
|
100
109
|
if (!rows.has(id)) {
|
|
101
|
-
out.push({ kind: 'unclassified', msg: `${id}: catalog entry missing from § Detector status` });
|
|
110
|
+
out.push({ kind: 'unclassified', id, msg: `${id}: catalog entry missing from § Detector status` });
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
113
|
// B — every classified row names a real entry.
|
|
105
114
|
for (const id of rows.keys()) {
|
|
106
115
|
if (!seen.has(id)) {
|
|
107
|
-
out.push({ kind: 'orphan-status', msg: `${id}: § Detector status row names an entry that no catalog table defines` });
|
|
116
|
+
out.push({ kind: 'orphan-status', id, msg: `${id}: § Detector status row names an entry that no catalog table defines` });
|
|
108
117
|
}
|
|
109
118
|
}
|
|
110
119
|
// D + E — status vocabulary and reasons.
|
|
@@ -112,12 +121,13 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
|
|
|
112
121
|
if (!(STATUSES as readonly string[]).includes(row.status)) {
|
|
113
122
|
out.push({
|
|
114
123
|
kind: 'bad-status',
|
|
124
|
+
id,
|
|
115
125
|
msg: `${id}: status '${row.status || '(empty)'}' is not one of ${STATUSES.join(', ')}`,
|
|
116
126
|
});
|
|
117
127
|
continue;
|
|
118
128
|
}
|
|
119
129
|
if (row.status !== 'backed' && row.note === '') {
|
|
120
|
-
out.push({ kind: 'missing-reason', msg: `${id}: status '${row.status}' carries no reason` });
|
|
130
|
+
out.push({ kind: 'missing-reason', id, msg: `${id}: status '${row.status}' carries no reason` });
|
|
121
131
|
}
|
|
122
132
|
}
|
|
123
133
|
|
|
@@ -126,7 +136,7 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
|
|
|
126
136
|
const registry = new Set(registryCatalogIds(registryTs));
|
|
127
137
|
for (const id of backed) {
|
|
128
138
|
if (!registry.has(id)) {
|
|
129
|
-
out.push({ kind: 'backed-without-rule', msg: `${id}: marked backed but no registry rule declares that catalogId` });
|
|
139
|
+
out.push({ kind: 'backed-without-rule', id, msg: `${id}: marked backed but no registry rule declares that catalogId` });
|
|
130
140
|
}
|
|
131
141
|
}
|
|
132
142
|
for (const id of registry) {
|
|
@@ -134,6 +144,7 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
|
|
|
134
144
|
const known = rows.get(id);
|
|
135
145
|
out.push({
|
|
136
146
|
kind: 'rule-without-backed',
|
|
147
|
+
id,
|
|
137
148
|
msg: known
|
|
138
149
|
? `${id}: a registry rule declares this catalogId but § Detector status marks it '${known.status}'`
|
|
139
150
|
: `${id}: a registry rule declares this catalogId and no catalog entry carries it`,
|
|
@@ -167,8 +178,31 @@ export function main(): number {
|
|
|
167
178
|
const registryTs = fs.readFileSync(REGISTRY, 'utf-8');
|
|
168
179
|
const findings = parityFindings(catalogMd, registryTs);
|
|
169
180
|
|
|
181
|
+
const rows = statusRows(catalogMd);
|
|
170
182
|
const entries = catalogEntries(catalogMd).length;
|
|
171
|
-
const backed = [...
|
|
183
|
+
const backed = [...rows.values()].filter((r) => r.status === 'backed').length;
|
|
184
|
+
|
|
185
|
+
// Per-target completeness accounting. The target is a catalog ID, and the
|
|
186
|
+
// planned set is the UNION of the three places an id can appear — catalog
|
|
187
|
+
// tables, the § Detector status table, and the rule registry — because this
|
|
188
|
+
// gate's whole subject is parity BETWEEN those sets. Planning only the
|
|
189
|
+
// catalog would leave an orphan status row or a registry-only id
|
|
190
|
+
// unaccounted, which is the direction the gate exists to catch.
|
|
191
|
+
const ledger = new GateLedger('lint_design_antipattern_parity');
|
|
192
|
+
const targets = [
|
|
193
|
+
...new Set([
|
|
194
|
+
...catalogEntries(catalogMd),
|
|
195
|
+
...rows.keys(),
|
|
196
|
+
...registryCatalogIds(registryTs),
|
|
197
|
+
]),
|
|
198
|
+
].sort();
|
|
199
|
+
ledger.plan(targets);
|
|
200
|
+
const failedIds = new Set(findings.map((f) => f.id).filter((v): v is string => v !== undefined));
|
|
201
|
+
for (const id of targets) {
|
|
202
|
+
if (failedIds.has(id)) ledger.fail(id, 'parity finding');
|
|
203
|
+
else ledger.complete(id);
|
|
204
|
+
}
|
|
205
|
+
ledger.report();
|
|
172
206
|
|
|
173
207
|
if (findings.length === 0) {
|
|
174
208
|
_print(`✅ catalog ↔ rule parity OK — ${entries} entries classified, ${backed} detector-backed`);
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence artifacts declare what kind of evidence they are
|
|
3
|
+
* (road-to-release-review-p0 Phase 2).
|
|
4
|
+
*
|
|
5
|
+
* The contract is `docs/contracts/evidence-artifact-types.md`; this is its
|
|
6
|
+
* check. Read that document first — in particular its scope section, because
|
|
7
|
+
* the single most important property of this linter is what it deliberately
|
|
8
|
+
* does NOT do.
|
|
9
|
+
*
|
|
10
|
+
* **It is forward-looking, and there is no baseline file.** Measured by
|
|
11
|
+
* `--all` at the time of writing: 332 tracked markdown artifacts under
|
|
12
|
+
* `agents/evidence/`, 188 already resolving a type and 144 not.
|
|
13
|
+
* `check_completion_review` already refused to make one marker field required
|
|
14
|
+
* on the stated ground that it "would have been a migration event for the
|
|
15
|
+
* whole evidence corpus", and that reasoning is adopted rather than
|
|
16
|
+
* overturned. So the gate fires only on files ADDED relative to a base ref. A
|
|
17
|
+
* ratchet baseline was the obvious alternative and was rejected: it would mean
|
|
18
|
+
* committing a 144-entry suppression list, and a new baseline file in this
|
|
19
|
+
* repo trips two suppression gates of its own — paying a governance cost to
|
|
20
|
+
* enforce a rule the scope decision says should not be enforced retroactively.
|
|
21
|
+
*
|
|
22
|
+
* **Not registered in `gate-coverage.yml`, and the reason is the manifest's
|
|
23
|
+
* own.** A diff-scoped gate legitimately scans 0 on most runs, so it can carry
|
|
24
|
+
* no honest `min_scanned` floor, and `min_scanned: 0` is precisely the
|
|
25
|
+
* false-count shape that file rejects. `check_rule_projection_integrity` is
|
|
26
|
+
* the worked precedent: NOT registered, with the reason, ledger half
|
|
27
|
+
* discharged. This gate follows it — it adopts `_lib/gate_ledger.ts` below, so
|
|
28
|
+
* `check_gate_completeness` counts it as an adopter rather than a gap.
|
|
29
|
+
*
|
|
30
|
+
* **A declaration is read wherever the author already wrote one.** Three of
|
|
31
|
+
* the five types are pre-existing line grammars owned by
|
|
32
|
+
* `check_completion_review.ts`, and this linter imports those parsers rather
|
|
33
|
+
* than re-implementing them. Re-declaring an artifact that already carries
|
|
34
|
+
* `<!-- completion-review: v1 … -->` under a second `evidence-type:` marker
|
|
35
|
+
* would be the ambiguity the contract exists to remove.
|
|
36
|
+
*
|
|
37
|
+
* Modes:
|
|
38
|
+
* --new-only <base-ref> gate: fail on an ADDED artifact with no type
|
|
39
|
+
* --all report: corpus census, always exit 0
|
|
40
|
+
* --quiet verdict line only
|
|
41
|
+
*
|
|
42
|
+
* Exit 0 = clean (or report mode). Exit 1 = an added artifact carries no
|
|
43
|
+
* resolvable type. Exit 2 = the linter could not run (bad ref, no git).
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { execFileSync } from 'node:child_process';
|
|
47
|
+
import fs from 'node:fs';
|
|
48
|
+
import path from 'node:path';
|
|
49
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
50
|
+
|
|
51
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
52
|
+
import { resolveBaseRef } from './_lib/ratchet_base_ref.js';
|
|
53
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
54
|
+
import { parseHonestNull, parseMarkerLine, parseSkipDeclaration } from './check_completion_review.js';
|
|
55
|
+
|
|
56
|
+
export const EVIDENCE_ROOT = path.join('agents', 'evidence');
|
|
57
|
+
|
|
58
|
+
/** The five types the contract defines. Anything else is an error, not a warning. */
|
|
59
|
+
export const EVIDENCE_TYPES = [
|
|
60
|
+
'original-review',
|
|
61
|
+
'current-binding',
|
|
62
|
+
'declared-skip',
|
|
63
|
+
'honest-null',
|
|
64
|
+
'analysis',
|
|
65
|
+
] as const;
|
|
66
|
+
|
|
67
|
+
export type EvidenceType = (typeof EVIDENCE_TYPES)[number];
|
|
68
|
+
|
|
69
|
+
/** The explicit marker, for the two types with no pre-existing grammar. */
|
|
70
|
+
const TYPE_MARKER_RE = /^<!--\s*evidence-type:\s*([a-z-]+)\s*-->$/;
|
|
71
|
+
|
|
72
|
+
/** How many leading lines are scanned for the marker. A type belongs at the top. */
|
|
73
|
+
const MARKER_SCAN_LINES = 40;
|
|
74
|
+
|
|
75
|
+
export interface TypeResolution {
|
|
76
|
+
readonly type: EvidenceType | null;
|
|
77
|
+
/** Which declaration produced it — named so a reader can check the claim. */
|
|
78
|
+
readonly via: 'marker' | 'completion-review' | 'honest-null' | 'skip' | 'review-input-path' | null;
|
|
79
|
+
/** Set when a marker was present but named something outside the set. */
|
|
80
|
+
readonly invalidMarker: string | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve one artifact's declared type.
|
|
85
|
+
*
|
|
86
|
+
* Order matters and encodes the contract: the pre-existing grammars are
|
|
87
|
+
* checked FIRST, so an artifact that already declares itself through
|
|
88
|
+
* `check_completion_review`'s vocabulary is never asked for a second marker.
|
|
89
|
+
* The path rule is last and is the contract's one acknowledged exception.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveEvidenceType(relPath: string, contents: string): TypeResolution {
|
|
92
|
+
// BOTH scans share one window. They did not: the grammar scan walked the
|
|
93
|
+
// whole file while the marker scan stopped at 40 lines, so a quoted
|
|
94
|
+
// `completion-review` line deep in a prose artifact silently overrode an
|
|
95
|
+
// explicit `evidence-type:` marker its author had written at the top
|
|
96
|
+
// (R2 finding 8). A declaration belongs near the top of the file either
|
|
97
|
+
// way, and one window makes the precedence readable instead of positional.
|
|
98
|
+
const lines = contents.split('\n').slice(0, MARKER_SCAN_LINES);
|
|
99
|
+
|
|
100
|
+
for (const raw of lines) {
|
|
101
|
+
const line = raw.trim();
|
|
102
|
+
if (line === '') {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (parseMarkerLine(line) !== null) {
|
|
106
|
+
return { type: 'current-binding', via: 'completion-review', invalidMarker: null };
|
|
107
|
+
}
|
|
108
|
+
if (parseHonestNull(line) !== null) {
|
|
109
|
+
return { type: 'honest-null', via: 'honest-null', invalidMarker: null };
|
|
110
|
+
}
|
|
111
|
+
if (parseSkipDeclaration(line) !== null) {
|
|
112
|
+
return { type: 'declared-skip', via: 'skip', invalidMarker: null };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const raw of lines) {
|
|
117
|
+
const m = TYPE_MARKER_RE.exec(raw.trim());
|
|
118
|
+
if (m === null) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const value = m[1] as string;
|
|
122
|
+
if ((EVIDENCE_TYPES as readonly string[]).includes(value)) {
|
|
123
|
+
return { type: value as EvidenceType, via: 'marker', invalidMarker: null };
|
|
124
|
+
}
|
|
125
|
+
// A misspelled type reads as untyped to every consumer, which is the
|
|
126
|
+
// state the contract exists to end — so it is louder than absence,
|
|
127
|
+
// not quieter.
|
|
128
|
+
return { type: null, via: null, invalidMarker: value };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The contract's single filename-derived case. Safe because the directory
|
|
132
|
+
// is written by `dispatch_r2_reviewer.ts` and by nothing else, so the path
|
|
133
|
+
// IS the declaration rather than a heuristic about one.
|
|
134
|
+
// Split on `/`, never `path.sep`. Every caller feeds paths from
|
|
135
|
+
// `git diff --name-only` / `git ls-files`, and git emits `/`-separated
|
|
136
|
+
// paths on every platform — so on Windows `path.sep` (`\`) made the whole
|
|
137
|
+
// path one segment and this branch could never match (R2 finding 10).
|
|
138
|
+
// `\` is accepted too so a hand-built Windows path still resolves.
|
|
139
|
+
if (relPath.split(/[/\\]/).some((seg) => seg.endsWith('.review-input'))) {
|
|
140
|
+
return { type: 'original-review', via: 'review-input-path', invalidMarker: null };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { type: null, via: null, invalidMarker: null };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _git(root: string, args: readonly string[]): string {
|
|
147
|
+
return execFileSync('git', [...args], { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Markdown files ADDED under `agents/evidence/` relative to `baseRef`. */
|
|
151
|
+
export function addedEvidenceFiles(root: string, baseRef: string): string[] {
|
|
152
|
+
const out = _git(root, [
|
|
153
|
+
'diff',
|
|
154
|
+
'--name-only',
|
|
155
|
+
'--diff-filter=A',
|
|
156
|
+
`${baseRef}...HEAD`,
|
|
157
|
+
'--',
|
|
158
|
+
`${EVIDENCE_ROOT}/`,
|
|
159
|
+
]);
|
|
160
|
+
return out
|
|
161
|
+
.split('\n')
|
|
162
|
+
.map((s) => s.trim())
|
|
163
|
+
.filter((s) => s !== '' && s.endsWith('.md'));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Every markdown artifact in the corpus, for the census mode. */
|
|
167
|
+
export function allEvidenceFiles(root: string): string[] {
|
|
168
|
+
const out = _git(root, ['ls-files', '--', `${EVIDENCE_ROOT}/`]);
|
|
169
|
+
return out
|
|
170
|
+
.split('\n')
|
|
171
|
+
.map((s) => s.trim())
|
|
172
|
+
.filter((s) => s !== '' && s.endsWith('.md'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface Finding {
|
|
176
|
+
readonly file: string;
|
|
177
|
+
readonly reason: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function checkFiles(root: string, files: readonly string[], ledger?: GateLedger): Finding[] {
|
|
181
|
+
const findings: Finding[] = [];
|
|
182
|
+
ledger?.plan([...files]);
|
|
183
|
+
for (const rel of files) {
|
|
184
|
+
let contents: string;
|
|
185
|
+
try {
|
|
186
|
+
contents = fs.readFileSync(path.join(root, rel), 'utf8');
|
|
187
|
+
} catch {
|
|
188
|
+
// A file listed by git but unreadable on disk is not this
|
|
189
|
+
// linter's finding to report — a deleted-then-re-added path in a
|
|
190
|
+
// dirty tree hits this and is somebody else's gate.
|
|
191
|
+
ledger?.outOfScope(rel, 'no_applicable_files');
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const res = resolveEvidenceType(rel, contents);
|
|
195
|
+
if (res.invalidMarker !== null) {
|
|
196
|
+
ledger?.fail(rel, `invalid evidence-type: ${res.invalidMarker}`);
|
|
197
|
+
findings.push({
|
|
198
|
+
file: rel,
|
|
199
|
+
reason:
|
|
200
|
+
`evidence-type: ${res.invalidMarker} is not one of ` +
|
|
201
|
+
`${EVIDENCE_TYPES.join(' | ')}`,
|
|
202
|
+
});
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (res.type === null) {
|
|
206
|
+
ledger?.fail(rel, 'no evidence type declared');
|
|
207
|
+
findings.push({
|
|
208
|
+
file: rel,
|
|
209
|
+
reason:
|
|
210
|
+
'no evidence type declared — add `<!-- evidence-type: analysis -->` ' +
|
|
211
|
+
'(or the type that fits) per docs/contracts/evidence-artifact-types.md',
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
ledger?.complete(rel);
|
|
216
|
+
}
|
|
217
|
+
return findings;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function _argValue(flag: string): string | null {
|
|
221
|
+
const i = process.argv.indexOf(flag);
|
|
222
|
+
if (i === -1) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const v = process.argv[i + 1];
|
|
226
|
+
return v === undefined || v.startsWith('--') ? null : v;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function main(): number {
|
|
230
|
+
const quiet = process.argv.includes('--quiet');
|
|
231
|
+
const root = process.cwd();
|
|
232
|
+
const out = (s: string): void => {
|
|
233
|
+
if (!quiet) {
|
|
234
|
+
process.stdout.write(`${s}\n`);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
if (process.argv.includes('--all')) {
|
|
239
|
+
let files: string[];
|
|
240
|
+
try {
|
|
241
|
+
files = allEvidenceFiles(root);
|
|
242
|
+
} catch (exc) {
|
|
243
|
+
process.stdout.write(`lint_evidence_artifacts: cannot list ${EVIDENCE_ROOT} — ${String(exc)}\n`);
|
|
244
|
+
return 2;
|
|
245
|
+
}
|
|
246
|
+
const ledger = new GateLedger('lint_evidence_artifacts');
|
|
247
|
+
const findings = checkFiles(root, files, ledger);
|
|
248
|
+
ledger.report();
|
|
249
|
+
// Census mode reads the whole corpus, so a zero here IS blindness —
|
|
250
|
+
// no `allowEmpty`, deliberately. The gate mode below is the opposite
|
|
251
|
+
// case and says so on its own line.
|
|
252
|
+
reportScanned({
|
|
253
|
+
gate: 'lint_evidence_artifacts --all',
|
|
254
|
+
scanned: files.length,
|
|
255
|
+
units: 'evidence artifacts',
|
|
256
|
+
roots: [EVIDENCE_ROOT],
|
|
257
|
+
});
|
|
258
|
+
const typed = files.length - findings.length;
|
|
259
|
+
out(`lint_evidence_artifacts — census over ${String(files.length)} artifact(s)`);
|
|
260
|
+
out(` typed ${String(typed)}`);
|
|
261
|
+
out(` untyped ${String(findings.length)}`);
|
|
262
|
+
// Report mode, never a gate: the pre-existing untyped corpus is a
|
|
263
|
+
// deliberate scope exclusion, so publishing the number is the point
|
|
264
|
+
// and failing on it would contradict the contract.
|
|
265
|
+
process.stdout.write(
|
|
266
|
+
`lint_evidence_artifacts: census — ${String(typed)} typed, ${String(findings.length)} untyped (report only)\n`,
|
|
267
|
+
);
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// A bare invocation IS the gate, resolving its own base. Requiring an
|
|
272
|
+
// explicit ref would have made the roadmap's own `verify:` annotation exit
|
|
273
|
+
// 2 on a usage error, and a check whose default invocation is a usage
|
|
274
|
+
// error is a check nobody runs. `--new-only <ref>` stays as the override.
|
|
275
|
+
//
|
|
276
|
+
// `resolveBaseRef` rather than a hardcoded `origin/main`: `actions/checkout`
|
|
277
|
+
// performs a shallow PR-merge fetch, so a PR build frequently has no
|
|
278
|
+
// `origin/main` remote-tracking ref at all. That ladder already handles it
|
|
279
|
+
// and is the repo's one answer to this question.
|
|
280
|
+
const baseRef = _argValue('--new-only') ?? resolveBaseRef(root);
|
|
281
|
+
if (baseRef === null) {
|
|
282
|
+
// Fail rather than compare against an assumed-empty base — the same
|
|
283
|
+
// default `resolveBaseRef`'s own docstring names. An unresolvable base
|
|
284
|
+
// would otherwise make every artifact look un-added and the gate would
|
|
285
|
+
// pass by scanning nothing.
|
|
286
|
+
process.stdout.write(
|
|
287
|
+
'lint_evidence_artifacts: no base ref resolved — pass --new-only <ref>, ' +
|
|
288
|
+
'set RATCHET_BASE_REF, or run --all for the census\n',
|
|
289
|
+
);
|
|
290
|
+
return 2;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let files: string[];
|
|
294
|
+
try {
|
|
295
|
+
files = addedEvidenceFiles(root, baseRef);
|
|
296
|
+
} catch (exc) {
|
|
297
|
+
process.stdout.write(`lint_evidence_artifacts: cannot diff against ${baseRef} — ${String(exc)}\n`);
|
|
298
|
+
return 2;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const ledger = new GateLedger('lint_evidence_artifacts');
|
|
302
|
+
const findings = checkFiles(root, files, ledger);
|
|
303
|
+
ledger.report();
|
|
304
|
+
// A run that scans nothing exits green, and SAYS SO — silent green over an
|
|
305
|
+
// empty set is how a gate stops measuring anything without anyone
|
|
306
|
+
// noticing. The `allowEmpty` reason is `EMPTY_VALID` rather than
|
|
307
|
+
// `OPTIONAL_INPUT`: this gate measures a DIFF, not a corpus, so "no
|
|
308
|
+
// evidence artifact was added on this branch" genuinely is the success
|
|
309
|
+
// state and stays true even if `agents/evidence/` were deleted entirely.
|
|
310
|
+
reportScanned({
|
|
311
|
+
gate: 'lint_evidence_artifacts',
|
|
312
|
+
scanned: files.length,
|
|
313
|
+
units: 'added evidence artifacts',
|
|
314
|
+
roots: [EVIDENCE_ROOT],
|
|
315
|
+
allowEmpty:
|
|
316
|
+
'EMPTY_VALID: the scope is the set of artifacts ADDED since the base ref; ' +
|
|
317
|
+
'a branch that adds none has nothing to type, and zero is the pass.',
|
|
318
|
+
});
|
|
319
|
+
if (files.length === 0) {
|
|
320
|
+
process.stdout.write(
|
|
321
|
+
`lint_evidence_artifacts: ✅ no evidence artifact added since ${baseRef} — nothing to check\n`,
|
|
322
|
+
);
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
out(`lint_evidence_artifacts — ${String(files.length)} added artifact(s) since ${baseRef}`);
|
|
326
|
+
if (findings.length === 0) {
|
|
327
|
+
process.stdout.write(
|
|
328
|
+
`lint_evidence_artifacts: ✅ all ${String(files.length)} added artifact(s) declare a type\n`,
|
|
329
|
+
);
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
process.stdout.write(`lint_evidence_artifacts: ❌ ${String(findings.length)} untyped artifact(s)\n`);
|
|
333
|
+
for (const f of findings) {
|
|
334
|
+
process.stdout.write(` ${f.file}\n ${f.reason}\n`);
|
|
335
|
+
}
|
|
336
|
+
return 1;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
340
|
+
function _isCliEntry(): boolean {
|
|
341
|
+
if (process.argv[1] === undefined) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
345
|
+
if (import.meta.url === argvUrl) {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
350
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
351
|
+
return here === argv;
|
|
352
|
+
} catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
358
|
+
process.exit(main());
|
|
359
|
+
}
|
|
@@ -49,6 +49,7 @@ import * as fs from 'node:fs';
|
|
|
49
49
|
import * as path from 'node:path';
|
|
50
50
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
51
51
|
|
|
52
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
52
53
|
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
53
54
|
|
|
54
55
|
const _FILE = fileURLToPath(import.meta.url);
|
|
@@ -432,7 +433,40 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
432
433
|
const text = fs.readFileSync(ledgerPath, 'utf-8');
|
|
433
434
|
const { records, findings } = lintLedgerText(text, REPO);
|
|
434
435
|
const citations = [...collectCitations(REPO), ...collectPersonaSources(REPO)];
|
|
435
|
-
const
|
|
436
|
+
const orphans = findOrphanCitations(citations, records);
|
|
437
|
+
const allFindings = [...findings, ...orphans];
|
|
438
|
+
|
|
439
|
+
// Per-target completeness accounting over the two populations this gate
|
|
440
|
+
// reasons about: the ledger's own rows, and the citations that point at
|
|
441
|
+
// them. Both are legitimately EMPTY today, and that is exactly why the
|
|
442
|
+
// accounting is worth having — a green line over zero rows and a green line
|
|
443
|
+
// over a hundred checked rows read identically without it, which is the
|
|
444
|
+
// failure the success message above already tries to talk its way out of in
|
|
445
|
+
// prose. The ledger states it structurally instead.
|
|
446
|
+
const ledger = new GateLedger('lint_harvest_provenance');
|
|
447
|
+
const known = new Set(records.map((r) => r.harvest_id));
|
|
448
|
+
// The row population is every PARSED ledger line, not `records`: a row that
|
|
449
|
+
// fails validation or uniqueness is `continue`d and never reaches `records`,
|
|
450
|
+
// so planning `records` would plan exactly the rows that passed and account
|
|
451
|
+
// for none of the ones that did not — an accounting that can only ever read
|
|
452
|
+
// 100 %. Lines are the stable key; a Finding already carries one.
|
|
453
|
+
const { parsed } = parseLedgerText(text);
|
|
454
|
+
const badLines = new Set(findings.filter((f) => f.line > 0).map((f) => f.line));
|
|
455
|
+
ledger.plan([
|
|
456
|
+
...parsed.map((r) => `row:${String(r.line)}`),
|
|
457
|
+
...citations.map((c) => `citation:${c.id}@${c.file}`),
|
|
458
|
+
]);
|
|
459
|
+
for (const r of parsed) {
|
|
460
|
+
const id = `row:${String(r.line)}`;
|
|
461
|
+
if (badLines.has(r.line)) ledger.fail(id, 'ledger-row finding');
|
|
462
|
+
else ledger.complete(id);
|
|
463
|
+
}
|
|
464
|
+
for (const c of citations) {
|
|
465
|
+
const id = `citation:${c.id}@${c.file}`;
|
|
466
|
+
if (known.has(c.id)) ledger.complete(id);
|
|
467
|
+
else ledger.fail(id, 'orphan citation — no ledger row carries this harvest id');
|
|
468
|
+
}
|
|
469
|
+
ledger.report();
|
|
436
470
|
|
|
437
471
|
if (allFindings.length > 0) {
|
|
438
472
|
for (const f of allFindings) {
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* (Status, Owner, Blocks, What to do, Resolved when).
|
|
10
10
|
* 2. Every inline `<!-- blocked-by: <id> -->` annotation resolves to a
|
|
11
11
|
* `### blocker: <id>` entry declared in the SAME file.
|
|
12
|
+
* 3. An entry that declares `- **Class:**` declares a class the taxonomy
|
|
13
|
+
* knows (0-3), and a class-0 or class-1 entry says HOW it runs
|
|
14
|
+
* (`- **Run:**`). A gate that claims to be executable without naming the
|
|
15
|
+
* command is the same defect as a decision with no option set: it reads
|
|
16
|
+
* as actionable and is not.
|
|
12
17
|
*
|
|
13
18
|
* Fenced code blocks are stripped before scanning so a roadmap that shows
|
|
14
19
|
* the `## Blockers` shape as a documentation example is not flagged.
|
|
@@ -63,6 +68,36 @@ const DECIDABILITY_FIELDS: ReadonlyArray<readonly [string, RegExp]> = [
|
|
|
63
68
|
['If you do nothing', /^-[ \t]*\*\*If you do nothing:\*\*[ \t]*\S/im],
|
|
64
69
|
];
|
|
65
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The gate taxonomy's four classes, as authored in the blocker entry.
|
|
73
|
+
*
|
|
74
|
+
* `Class` is deliberately OPTIONAL and its absence means class 3 — human-only.
|
|
75
|
+
* Making the safe class the default is what keeps a misclassification a
|
|
76
|
+
* reviewed edit rather than a runtime judgment: nothing an author forgets to
|
|
77
|
+
* write can become executable by omission.
|
|
78
|
+
*/
|
|
79
|
+
const CLASS_FIELD_RE = /^-[ \t]*\*\*Class:\*\*[ \t]*(.*)$/im;
|
|
80
|
+
const RUN_FIELD_RE = /^-[ \t]*\*\*Run:\*\*[ \t]*(\S.*)$/im;
|
|
81
|
+
const KNOWN_CLASSES: ReadonlySet<string> = new Set(['0', '1', '2', '3']);
|
|
82
|
+
/** The classes whose whole claim is that an agent can execute them. */
|
|
83
|
+
const RUNNABLE_CLASSES: ReadonlySet<string> = new Set(['0', '1']);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The authored class, or `''` when the entry declares none.
|
|
87
|
+
*
|
|
88
|
+
* Only the leading token is read, so `- **Class:** 1 — budget-preauthorized`
|
|
89
|
+
* parses as `1`: the taxonomy name is documentation for the reader and must
|
|
90
|
+
* not have to be spelled identically in every entry to stay valid.
|
|
91
|
+
*/
|
|
92
|
+
function _blockerClass(body: string): string {
|
|
93
|
+
const m = CLASS_FIELD_RE.exec(body);
|
|
94
|
+
if (!m) {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
const raw = ((m[1] as string) ?? '').trim();
|
|
98
|
+
return raw === '' ? '' : (raw.split(/[\s—–-]/)[0] as string);
|
|
99
|
+
}
|
|
100
|
+
|
|
66
101
|
/** `What to do:` body — everything up to the next field marker. */
|
|
67
102
|
const WHAT_TO_DO_SLICE_RE =
|
|
68
103
|
/^-[ \t]*\*\*What to do:\*\*([\s\S]*?)(?=^-[ \t]*\*\*[A-Z]|^###[ \t]|$(?![\s\S]))/im;
|
|
@@ -157,6 +192,33 @@ function _scanBoth(rawText: string): ScanResult {
|
|
|
157
192
|
// made would be churn, and the ratchet would never reach zero.
|
|
158
193
|
const isResolved = /^-[ \t]*\*\*Status:\*\*[ \t]*resolved/im.test(body);
|
|
159
194
|
if (!isResolved) {
|
|
195
|
+
// The class/run contract is HARD rather than ratcheted, and it
|
|
196
|
+
// can be: `Class:` is a new opt-in field, so on the day it
|
|
197
|
+
// ships no entry in the tree declares one and the rule fires on
|
|
198
|
+
// nothing. There is no backlog to grandfather — the "strict
|
|
199
|
+
// gate reds ~283 files" failure the decidability half had to
|
|
200
|
+
// avoid simply does not arise here.
|
|
201
|
+
const cls = _blockerClass(body);
|
|
202
|
+
if (cls !== '') {
|
|
203
|
+
const at = _lineAt(text, sectionStart + cur.start);
|
|
204
|
+
if (!KNOWN_CLASSES.has(cls)) {
|
|
205
|
+
violations.push({
|
|
206
|
+
line: at,
|
|
207
|
+
message:
|
|
208
|
+
`blocker '${cur.id}' declares unknown class '${cls}' ` +
|
|
209
|
+
'(expected 0 auto-run, 1 budget-preauthorized, ' +
|
|
210
|
+
'2 consent-once, or 3 human-only)',
|
|
211
|
+
});
|
|
212
|
+
} else if (RUNNABLE_CLASSES.has(cls) && !RUN_FIELD_RE.test(body)) {
|
|
213
|
+
violations.push({
|
|
214
|
+
line: at,
|
|
215
|
+
message:
|
|
216
|
+
`blocker '${cur.id}' is class ${cls} but declares no ` +
|
|
217
|
+
'**Run:** command — a gate that claims to be runnable ' +
|
|
218
|
+
'must say how',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
160
222
|
const gaps = DECIDABILITY_FIELDS.filter(([, re]) => !re.test(body)).map(
|
|
161
223
|
([name]) => name,
|
|
162
224
|
);
|
|
@@ -358,6 +420,9 @@ export {
|
|
|
358
420
|
ROADMAP_GLOB,
|
|
359
421
|
REQUIRED_FIELDS,
|
|
360
422
|
DECIDABILITY_FIELDS,
|
|
423
|
+
KNOWN_CLASSES,
|
|
424
|
+
RUNNABLE_CLASSES,
|
|
425
|
+
_blockerClass,
|
|
361
426
|
_hasExecutableSubstance,
|
|
362
427
|
_scan,
|
|
363
428
|
_scanBoth,
|
|
@@ -445,7 +445,13 @@ function _check_external_population_gates(text: string, warnings: string[]): voi
|
|
|
445
445
|
|
|
446
446
|
function _check_execution_mode(fm: string, problems: string[]): void {
|
|
447
447
|
if (!fm || !/^execution:/m.test(fm)) {
|
|
448
|
-
|
|
448
|
+
// Absent is VALID and stays valid — but it no longer means "interactive".
|
|
449
|
+
// Since road-to-user-out-of-the-loop Phase 1, an absent field is DERIVED
|
|
450
|
+
// by the `roadmap-process-loop § 3a` ladder from the invocation form, so
|
|
451
|
+
// the old comment here named a default that no longer exists. Nothing to
|
|
452
|
+
// check either way: this linter validates a declared value, and there is
|
|
453
|
+
// no declaration to validate.
|
|
454
|
+
return;
|
|
449
455
|
}
|
|
450
456
|
const m = EXEC_MODE_PAT.exec(fm);
|
|
451
457
|
if (m === null) {
|