@event4u/agent-config 15.0.0 → 16.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +301 -347
- package/CONTRIBUTING.md +9 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +26 -0
- package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
- package/dist/agent-src/commands/challenge-me/closure.md +178 -0
- package/dist/agent-src/commands/challenge-me.md +9 -2
- package/dist/agent-src/commands/council/default.md +25 -5
- package/dist/agent-src/commands/feature/plan.md +28 -1
- package/dist/agent-src/commands/feature/roadmap.md +28 -1
- package/dist/agent-src/commands/implement-ticket.md +27 -0
- package/dist/agent-src/commands/jira-ticket.md +27 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/pr/merge.md +60 -9
- package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
- package/dist/agent-src/commands/roadmap/create.md +77 -29
- package/dist/agent-src/commands/roadmap/materialize.md +27 -0
- package/dist/agent-src/commands/roadmap/process-full.md +123 -18
- package/dist/agent-src/commands/team.md +22 -0
- package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +97 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
- package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
- package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
- package/dist/agent-src/rules/autonomous-execution.md +15 -8
- package/dist/agent-src/rules/delegation-policy.md +23 -45
- package/dist/agent-src/rules/design-fidelity.md +32 -37
- package/dist/agent-src/rules/evaluator-independence.md +43 -49
- package/dist/agent-src/rules/test-first.md +41 -0
- package/dist/agent-src/rules/user-interaction.md +31 -30
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
- package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
- package/dist/agent-src/skills/database/data/manifest.json +4 -4
- package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
- package/dist/agent-src/skills/design-review/SKILL.md +50 -0
- package/dist/agent-src/skills/error-handling-patterns/SKILL.md +9 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
- package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
- package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
- package/dist/agent-src/templates/agent-settings.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +188 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/dist/cli-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
- package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
- package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
- package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
- package/dist/cli-delegate/chunk-NUHLI2RX.js +80 -0
- package/dist/cli-delegate/{chunk-HZ52BRGG.js → chunk-P6ZHP5W2.js} +15 -90
- package/dist/cli-delegate/{chunk-TYSY33MD.js → chunk-QJWAUMJW.js} +12 -2
- package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
- package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
- package/dist/cli-delegate/{chunk-2KKXJW25.js → chunk-S3VFXMTM.js} +1 -1
- package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
- package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
- package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +918 -325
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +5 -3
- package/dist/cli-delegate/cmd_export.js +9 -7
- package/dist/cli-delegate/cmd_fleet.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +6 -6
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +3 -2
- package/dist/cli-delegate/cmd_preflight.js +4 -2
- package/dist/cli-delegate/cmd_prune.js +10 -8
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +28 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
- package/dist/cli-delegate/cmd_settings_set.js +14 -1
- package/dist/cli-delegate/cmd_sync.js +10 -8
- package/dist/cli-delegate/cmd_uninstall.js +16 -13
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +12 -10
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +87 -40
- 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 +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +2601 -1073
- package/dist/install/claudeRuleRewrite.js +14 -3
- package/dist/install/conflict.js +60 -10
- package/dist/install/conflictTracker.js +218 -0
- package/dist/install/fsPrimitives.js +54 -0
- package/dist/install/install.mjs +2008 -1481
- package/dist/install/manifestFiles.js +259 -0
- package/dist/install/preflight.js +14 -2
- package/dist/install/preserve.js +157 -0
- package/dist/install/recordedOwnership.js +132 -0
- package/dist/install/txlog.js +11 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1612 -667
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +3 -2
- package/dist/scripts/_lib/fs_atomic.js +167 -0
- package/dist/scripts/_lib/installed_lock.js +1 -1
- package/dist/scripts/_lib/installed_tools.js +460 -0
- package/dist/scripts/_lib/lean_projection_mode.js +3 -2
- package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
- package/dist/server/io/yamlIO.js +50 -0
- package/dist/server/routes/install.js +8 -0
- package/dist/server/routes/wizard.js +9 -3
- package/dist/server/schemas/settings.js +17 -5
- package/dist/shared/missionExecution.js +97 -0
- package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +26 -7
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/docs/guidelines/design-fidelity-routing.md +29 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/src/config/agent-settings.template.yml +139 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +18 -5
- package/src/config/continuity-surface.json +28 -0
- package/src/config/gate-coverage.yml +146 -13
- package/src/config/gate-violation-baselines.json +6 -6
- package/src/config/hook-token-budget.json +2 -0
- package/src/config/host-injection-effect.json +1 -1
- package/src/config/loop-surfaces.yaml +215 -0
- package/src/config/platform-anchor.json +3 -2
- package/src/config/preamble-payload-budget.json +12 -7
- package/src/config/preamble-payload-exceptions.json +28 -0
- package/src/config/release-gate-locality.yml +11 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/install/claudeRuleRewrite.ts +16 -2
- package/src/install/conflict.ts +65 -9
- package/src/install/conflictTracker.ts +299 -0
- package/src/install/fsPrimitives.ts +54 -0
- package/src/install/manifestFiles.ts +312 -0
- package/src/install/preflight.ts +16 -2
- package/src/install/preserve.ts +205 -0
- package/src/install/recordedOwnership.ts +149 -0
- package/src/install/txlog.ts +11 -2
- package/src/install/types.ts +19 -4
- package/src/scripts/_cli/cmd_conformance.ts +88 -15
- package/src/scripts/_cli/cmd_doctor.ts +4 -3
- package/src/scripts/_cli/cmd_route_explain.ts +9 -2
- package/src/scripts/_cli/cmd_settings_get.ts +44 -0
- package/src/scripts/_cli/doctor_execution.ts +90 -0
- package/src/scripts/_dispatch.bash +3 -1
- package/src/scripts/_lib/activation_payload.ts +247 -0
- package/src/scripts/_lib/agent_settings.ts +4 -2
- package/src/scripts/_lib/authority_path.ts +102 -0
- package/src/scripts/_lib/base_ref_payload.ts +315 -0
- package/src/scripts/_lib/cascade_base.ts +94 -0
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- package/src/scripts/_lib/continuation_ladder.ts +149 -2
- package/src/scripts/_lib/council_transport.ts +129 -0
- package/src/scripts/_lib/delivery_ready.ts +112 -0
- package/src/scripts/_lib/dropped_decision.ts +142 -0
- package/src/scripts/_lib/forge_protection.ts +267 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- package/src/scripts/_lib/installed_lock.ts +1 -1
- package/src/scripts/_lib/json_python_parity.ts +127 -0
- package/src/scripts/_lib/lean_projection_mode.ts +14 -3
- package/src/scripts/_lib/loop_surfaces.ts +382 -0
- package/src/scripts/_lib/measured_payload_ceiling.ts +424 -0
- package/src/scripts/_lib/mission_record.ts +190 -0
- package/src/scripts/_lib/obligation_frequency.ts +217 -0
- package/src/scripts/_lib/obligations.ts +402 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/payload_catalogue_completeness.ts +211 -0
- package/src/scripts/_lib/release_findings_ingest.ts +324 -0
- package/src/scripts/_lib/release_highlights.ts +199 -10
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/release_tag_dispatch.ts +63 -0
- package/src/scripts/_lib/review_baseline.ts +254 -0
- package/src/scripts/_lib/review_skipped_record.ts +61 -1
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/standing_bound_ratchet.ts +216 -103
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/turn_end_refusals.ts +51 -4
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -0
- package/src/scripts/ai_council/config.ts +8 -11
- package/src/scripts/ai_council/fallback_config.ts +5 -3
- package/src/scripts/ai_council/ownership_classes.ts +118 -0
- package/src/scripts/ask_block_census.ts +285 -15
- package/src/scripts/check_beta_review_markers.ts +35 -4
- package/src/scripts/check_enforcement_matrix.ts +624 -0
- package/src/scripts/check_finding_dispositions.ts +167 -16
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +954 -0
- package/src/scripts/check_held_object_arrivals.ts +381 -0
- package/src/scripts/check_preamble_payload_budget.ts +474 -124
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +567 -0
- package/src/scripts/check_release_published.ts +1 -1
- package/src/scripts/check_reply_consistency.ts +86 -17
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/closure_scan.ts +333 -0
- package/src/scripts/compile_hook_manifest.ts +43 -3
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +127 -12
- package/src/scripts/hooks/block_kernel_rule_writes.ts +12 -2
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/design_pass_hook.ts +128 -3
- package/src/scripts/hooks/end_review_nudge_hook.ts +101 -21
- package/src/scripts/hooks/evidence_independence.ts +96 -0
- package/src/scripts/hooks/memory_recall_hook.ts +540 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
- package/src/scripts/hooks/review_baseline_hook.ts +130 -0
- package/src/scripts/hooks/rule_inject_hook.ts +49 -1
- package/src/scripts/hooks/run_continuation_hook.ts +17 -20
- package/src/scripts/hooks/turn_end_gate_hook.ts +232 -14
- package/src/scripts/hooks_doctor.ts +89 -1
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +107 -207
- package/src/scripts/interruption_report.ts +8 -2
- package/src/scripts/janitor.ts +15 -1
- package/src/scripts/lint_decision_classes.ts +464 -0
- package/src/scripts/lint_hook_manifest.ts +11 -0
- package/src/scripts/lint_major_migration_sections.ts +450 -0
- package/src/scripts/lint_plan_risk_register.ts +47 -3
- package/src/scripts/lint_roadmap_blockers.ts +61 -0
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/measure_turn_end_gate.ts +302 -5
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/release.ts +66 -162
- package/src/scripts/release_drill.ts +460 -15
- package/src/scripts/release_env.ts +1 -1
- package/src/scripts/release_publication.ts +52 -1
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_held_object_arrivals.ts +456 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/sync_agent_settings.ts +140 -18
- package/src/scripts/trigger_eval_grandfather.json +0 -1
- package/src/scripts/ui_conformance_probe.ts +622 -0
- package/src/server/io/yamlIO.ts +48 -0
- package/src/server/schemas/settings.ts +31 -7
- package/src/shared/missionExecution.ts +126 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* check_test_weakening — a test that asserts less than it did needs a verdict.
|
|
4
|
+
*
|
|
5
|
+
* `road-to-adversarial-verification-and-long-runs` Phase 2.4, second of two.
|
|
6
|
+
* `evaluator-independence` § Tests are evaluators forbids an implementer from
|
|
7
|
+
* silently weakening an assertion, deleting a failing test, skipping it, or
|
|
8
|
+
* loosening a threshold. This gate sees the shape of that in a diff.
|
|
9
|
+
*
|
|
10
|
+
* **NET counts, never raw removals — the whole gate turns on this.** Editing an
|
|
11
|
+
* assertion removes one line and adds another, so a gate counting removals reds
|
|
12
|
+
* every legitimate test edit. A gate that reds every PR gets its exemption
|
|
13
|
+
* widened until it finds nothing, which is Risk 4 of the roadmap that
|
|
14
|
+
* commissioned it and the allowlist-growth antipattern `autonomous-execution`
|
|
15
|
+
* already names. A NET loss is a different, much narrower claim: after this
|
|
16
|
+
* diff, the file asserts less than before.
|
|
17
|
+
*
|
|
18
|
+
* **What makes a weakening legitimate is an independent verdict**, and this gate
|
|
19
|
+
* checks only that one was COMMITTED in the same diff. Whether it actually
|
|
20
|
+
* contains an independent verdict is a judgement no gate can make, so it is not
|
|
21
|
+
* claimed here.
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* check_test_weakening [--diff-file F] [--files F …] [--base-ref REF]
|
|
25
|
+
* [--root DIR] [--quiet] [--self-test]
|
|
26
|
+
*
|
|
27
|
+
* Exit codes: 0 = pass (or discharged) · 1 = fail · 2 = usage error.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { spawnSync } from 'node:child_process';
|
|
31
|
+
import * as fs from 'node:fs';
|
|
32
|
+
import * as os from 'node:os';
|
|
33
|
+
import * as path from 'node:path';
|
|
34
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
35
|
+
|
|
36
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
37
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
38
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
39
|
+
import {
|
|
40
|
+
isTestPath,
|
|
41
|
+
isVerdictArtefact,
|
|
42
|
+
isWeakening,
|
|
43
|
+
parseUnifiedDiff,
|
|
44
|
+
weakeningSignal,
|
|
45
|
+
type WeakeningSignal,
|
|
46
|
+
} from './_lib/test_delta.js';
|
|
47
|
+
|
|
48
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
49
|
+
const REPO = path.resolve(path.dirname(_HERE), '..', '..');
|
|
50
|
+
|
|
51
|
+
export interface WeakeningReport {
|
|
52
|
+
readonly signals: readonly WeakeningSignal[];
|
|
53
|
+
readonly discharged: boolean;
|
|
54
|
+
readonly scanned: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The decision, pure: which test files weakened, and was a verdict committed. */
|
|
58
|
+
export function analyse(diff: string, changedFiles: readonly string[]): WeakeningReport {
|
|
59
|
+
const files = parseUnifiedDiff(diff).filter((f) => isTestPath(f.path));
|
|
60
|
+
const signals = files.map(weakeningSignal).filter(isWeakening);
|
|
61
|
+
return {
|
|
62
|
+
signals,
|
|
63
|
+
discharged: changedFiles.some(isVerdictArtefact),
|
|
64
|
+
scanned: files.length,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One human-readable line per weakened file. */
|
|
69
|
+
export function renderSignal(s: WeakeningSignal): string {
|
|
70
|
+
const parts: string[] = [];
|
|
71
|
+
if (s.assertionsLost > 0) parts.push(`${String(s.assertionsLost)} assertion(s) net-removed`);
|
|
72
|
+
if (s.suppressionsGained > 0) {
|
|
73
|
+
parts.push(`${String(s.suppressionsGained)} suppression(s) net-added`);
|
|
74
|
+
}
|
|
75
|
+
return `${s.path}: ${parts.join(' · ')}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function git(args: readonly string[], root: string): string | null {
|
|
79
|
+
const r = spawnSync('git', [...args], { cwd: root, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
80
|
+
return r.status === 0 ? r.stdout : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveBaseRef(explicit: string | null, root: string): string {
|
|
84
|
+
if (explicit !== null) return explicit;
|
|
85
|
+
for (const ref of ['origin/main', 'origin/master', 'main', 'master']) {
|
|
86
|
+
if (git(['rev-parse', '--verify', '--quiet', ref], root) !== null) return ref;
|
|
87
|
+
}
|
|
88
|
+
return 'HEAD~1';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function usage(): string {
|
|
92
|
+
return (
|
|
93
|
+
'usage: check_test_weakening [--diff-file F] [--files F …] [--base-ref REF]\n' +
|
|
94
|
+
' [--root DIR] [--quiet] [--self-test]\n'
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
99
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
100
|
+
process.stdout.write(usage());
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
if (argv.includes('--self-test')) return selfTest();
|
|
104
|
+
|
|
105
|
+
const quiet = argv.includes('--quiet');
|
|
106
|
+
const at = (flag: string): string | null => {
|
|
107
|
+
const i = argv.indexOf(flag);
|
|
108
|
+
return i >= 0 ? (argv[i + 1] ?? null) : null;
|
|
109
|
+
};
|
|
110
|
+
const root = at('--root') ?? REPO;
|
|
111
|
+
|
|
112
|
+
let diff: string;
|
|
113
|
+
let changed: string[];
|
|
114
|
+
const diffFile = at('--diff-file');
|
|
115
|
+
const fi = argv.indexOf('--files');
|
|
116
|
+
if (diffFile !== null) {
|
|
117
|
+
try {
|
|
118
|
+
diff = fs.readFileSync(diffFile, 'utf8');
|
|
119
|
+
} catch {
|
|
120
|
+
process.stderr.write(`check_test_weakening: cannot read ${diffFile}\n`);
|
|
121
|
+
return 2;
|
|
122
|
+
}
|
|
123
|
+
changed = fi >= 0 ? argv.slice(fi + 1).filter((a) => !a.startsWith('--')) : [];
|
|
124
|
+
} else {
|
|
125
|
+
const base = resolveBaseRef(at('--base-ref'), root);
|
|
126
|
+
const d = git(['diff', '--unified=0', `${base}...HEAD`], root);
|
|
127
|
+
const names = git(['diff', '--name-only', `${base}...HEAD`], root);
|
|
128
|
+
if (d === null || names === null) {
|
|
129
|
+
process.stderr.write('check_test_weakening: git diff failed\n');
|
|
130
|
+
return 2;
|
|
131
|
+
}
|
|
132
|
+
diff = d;
|
|
133
|
+
changed = names.split('\n').map((s) => s.trim()).filter((s) => s !== '');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Per-file accounting: a changed test file either measured clean, measured
|
|
137
|
+
// weakened, or was discharged by a committed verdict. Without it, "no test
|
|
138
|
+
// file changed" and "every test file was skipped" print the same green line.
|
|
139
|
+
const parsed = parseUnifiedDiff(diff);
|
|
140
|
+
const ledger = new GateLedger('check_test_weakening');
|
|
141
|
+
ledger.plan(parsed.map((f) => `file:${f.path}`));
|
|
142
|
+
const report = analyse(diff, changed);
|
|
143
|
+
const weakened = new Set(report.signals.map((s) => s.path));
|
|
144
|
+
for (const f of parsed) {
|
|
145
|
+
const target = `file:${f.path}`;
|
|
146
|
+
if (!isTestPath(f.path)) {
|
|
147
|
+
ledger.outOfScope(target, 'not_applicable_kind');
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (!weakened.has(f.path)) ledger.complete(target);
|
|
151
|
+
else if (report.discharged) ledger.outOfScope(target, 'declared_exemption');
|
|
152
|
+
else ledger.fail(target, renderSignal(weakeningSignal(f)));
|
|
153
|
+
}
|
|
154
|
+
ledger.report();
|
|
155
|
+
|
|
156
|
+
reportScanned({
|
|
157
|
+
gate: 'check_test_weakening',
|
|
158
|
+
scanned: report.scanned,
|
|
159
|
+
units: 'changed test file(s)',
|
|
160
|
+
roots: ['<the diff against the base ref>'],
|
|
161
|
+
// A diff touching no test file is the normal case for a docs or config
|
|
162
|
+
// change, and reporting it as a dead scope would make the gate cry wolf
|
|
163
|
+
// on most of the PRs it runs against.
|
|
164
|
+
allowEmpty: 'EMPTY_VALID: a diff that changes no test file has nothing to weaken',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (report.signals.length === 0) {
|
|
168
|
+
if (!quiet) {
|
|
169
|
+
process.stdout.write(
|
|
170
|
+
`✅ check_test_weakening: ${String(report.scanned)} test file(s), no net weakening\n`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (report.discharged) {
|
|
177
|
+
process.stdout.write(
|
|
178
|
+
`✅ check_test_weakening: ${String(report.signals.length)} weakened file(s), ` +
|
|
179
|
+
'discharged by an independent-verdict artefact under agents/evidence/reviews/\n',
|
|
180
|
+
);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
process.stderr.write(
|
|
185
|
+
`❌ check_test_weakening: ${String(report.signals.length)} test file(s) assert less ` +
|
|
186
|
+
'than before, with no independent-verdict artefact in this diff:\n' +
|
|
187
|
+
report.signals.map((s) => ` - ${renderSignal(s)}\n`).join('') +
|
|
188
|
+
' An implementer may not weaken a test silently (src/rules/evaluator-independence.md\n' +
|
|
189
|
+
' § Tests are evaluators). Where the test is genuinely wrong: evidence → independent\n' +
|
|
190
|
+
' test review → council or team, then commit the verdict under agents/evidence/reviews/.\n',
|
|
191
|
+
);
|
|
192
|
+
return 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function selfTest(): number {
|
|
196
|
+
const withDiff = (diff: string, files: readonly string[] = []): number => {
|
|
197
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ctw-'));
|
|
198
|
+
const f = path.join(dir, 'd.diff');
|
|
199
|
+
fs.writeFileSync(f, diff);
|
|
200
|
+
try {
|
|
201
|
+
const args = ['--diff-file', f, '--quiet'];
|
|
202
|
+
if (files.length > 0) args.push('--files', ...files);
|
|
203
|
+
return runGateCli(REPO, 'src/scripts/check_test_weakening.ts', args, dir);
|
|
204
|
+
} finally {
|
|
205
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const header = (p: string): string => `diff --git a/${p} b/${p}\n--- a/${p}\n+++ b/${p}\n@@\n`;
|
|
209
|
+
|
|
210
|
+
return runSelfTest({
|
|
211
|
+
gate: 'check_test_weakening',
|
|
212
|
+
minCases: 6,
|
|
213
|
+
minRejectCases: 3,
|
|
214
|
+
cases: [
|
|
215
|
+
{
|
|
216
|
+
name: 'a net-removed assertion is REJECTED',
|
|
217
|
+
expect: 'reject',
|
|
218
|
+
run: () =>
|
|
219
|
+
withDiff(header('tests/a.test.ts') + "- expect(x).toBe(1);\n"),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'an added it.skip is REJECTED',
|
|
223
|
+
expect: 'reject',
|
|
224
|
+
run: () => withDiff(header('tests/a.test.ts') + "+ it.skip('x', () => {});\n"),
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'an added @pytest.mark.skip is REJECTED — not a one-framework gate',
|
|
228
|
+
expect: 'reject',
|
|
229
|
+
run: () => withDiff(header('tests/a_test.py') + '+@pytest.mark.skip\n'),
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: 'EDITING an assertion is not weakening — the net is zero',
|
|
233
|
+
expect: 'accept',
|
|
234
|
+
run: () =>
|
|
235
|
+
withDiff(
|
|
236
|
+
header('tests/a.test.ts') +
|
|
237
|
+
"- expect(x).toBe(1);\n+ expect(x).toBe(2);\n",
|
|
238
|
+
),
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'ADDING assertions is never weakening',
|
|
242
|
+
expect: 'accept',
|
|
243
|
+
run: () =>
|
|
244
|
+
withDiff(
|
|
245
|
+
header('tests/a.test.ts') +
|
|
246
|
+
"+ expect(x).toBe(1);\n+ expect(y).toBe(2);\n",
|
|
247
|
+
),
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'DELETING a whole test file is the maximal weakening, and REDS',
|
|
251
|
+
expect: 'reject',
|
|
252
|
+
run: () =>
|
|
253
|
+
withDiff(
|
|
254
|
+
'diff --git a/tests/a.test.ts b/tests/a.test.ts\n' +
|
|
255
|
+
'--- a/tests/a.test.ts\n+++ /dev/null\n@@\n' +
|
|
256
|
+
'-expect(x).toBe(1);\n',
|
|
257
|
+
),
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'a removed assertion in PRODUCTION code is out of scope',
|
|
261
|
+
expect: 'accept',
|
|
262
|
+
run: () => withDiff(header('src/a.ts') + '- expect(x).toBe(1);\n'),
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: 'REMOVING a skip is the opposite of weakening',
|
|
266
|
+
expect: 'accept',
|
|
267
|
+
run: () => withDiff(header('tests/a.test.ts') + "- it.skip('x', () => {});\n"),
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'a DIFF FIXTURE held as string data is not a suppression',
|
|
271
|
+
expect: 'accept',
|
|
272
|
+
run: () =>
|
|
273
|
+
withDiff(
|
|
274
|
+
header('tests/gate.test.ts') +
|
|
275
|
+
'+ \'+@pytest.mark.skip\\n\',\n',
|
|
276
|
+
),
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: '…and the same construct written as CODE still reds',
|
|
280
|
+
expect: 'reject',
|
|
281
|
+
run: () => withDiff(header('tests/gate.test.ts') + '+@pytest.mark.skip\n'),
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: 'a committed verdict artefact discharges a real weakening',
|
|
285
|
+
expect: 'accept',
|
|
286
|
+
run: () =>
|
|
287
|
+
withDiff(header('tests/a.test.ts') + '- expect(x).toBe(1);\n', [
|
|
288
|
+
'agents/evidence/reviews/some-review.md',
|
|
289
|
+
]),
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* c8 ignore start — CLI entry, exercised by the self-test rather than in-process. */
|
|
296
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1] ?? '')).href) {
|
|
297
|
+
process.exit(main());
|
|
298
|
+
}
|
|
299
|
+
/* c8 ignore stop */
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* The closure detector — finds the open decisions in a plan, and classifies
|
|
4
|
+
* each one by OWNERSHIP. It never resolves one.
|
|
5
|
+
*
|
|
6
|
+
* That boundary is the whole design. A decision dispatcher was killed on
|
|
7
|
+
* purpose: no TypeScript path routes a decision, and this script adds none.
|
|
8
|
+
* What it adds is the half that can be mechanical — *which questions are open,
|
|
9
|
+
* and whose they are* — so the claim "this plan reached execution with twelve
|
|
10
|
+
* open technical decisions and asked the owner none of them" is falsifiable
|
|
11
|
+
* rather than a feeling.
|
|
12
|
+
*
|
|
13
|
+
* Detector families, each a phrase an author writes when they KNOW something
|
|
14
|
+
* is open. Deliberately narrow: a detector that fires on ordinary plan prose
|
|
15
|
+
* turns every producer red at once, and the cheapest repair is then to weaken
|
|
16
|
+
* it until it finds nothing.
|
|
17
|
+
*
|
|
18
|
+
* `tbd` TBD / to be decided / decide later
|
|
19
|
+
* `unpicked-alternative` "either X or Y", "option A … option B"
|
|
20
|
+
* `unchecked-assumption` "assuming", "presumably", "probably fine"
|
|
21
|
+
* `missing-verify` an open step with no `verify:` line
|
|
22
|
+
* `ambiguous-acceptance` a vague acceptance criterion (improve / clean up
|
|
23
|
+
* / best practices / handle errors properly)
|
|
24
|
+
* `contradictory` "contradicts", "conflicts with", "but also"
|
|
25
|
+
* `product-semantics` two valid user-visible outcomes
|
|
26
|
+
* `typed-op` an operation from the typed vocabulary
|
|
27
|
+
* `spend` a ceiling or quota question
|
|
28
|
+
*
|
|
29
|
+
* Ownership classification is the output that matters. Six families classify
|
|
30
|
+
* to a technical class the agent, an independent session, a council or a team
|
|
31
|
+
* resolves; exactly two reach the owner (`product-semantics` → `product-owned`,
|
|
32
|
+
* `typed-op` → `destructive-owned`); `spend` reaches nobody — it pauses and
|
|
33
|
+
* reports (ADR-268 § 8).
|
|
34
|
+
*
|
|
35
|
+
* Exit codes: 0 no open decisions · 1 open decisions found · 2 bad argv.
|
|
36
|
+
* Finding open decisions is the POINT, so `--strict` is what makes exit 1 a
|
|
37
|
+
* failure for a caller that wants one; the default is a report.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import * as fs from 'node:fs';
|
|
41
|
+
import * as path from 'node:path';
|
|
42
|
+
import process from 'node:process';
|
|
43
|
+
import { fileURLToPath } from 'node:url';
|
|
44
|
+
|
|
45
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
46
|
+
const _PROG = 'closure_scan';
|
|
47
|
+
|
|
48
|
+
export type Kind =
|
|
49
|
+
| 'tbd'
|
|
50
|
+
| 'unpicked-alternative'
|
|
51
|
+
| 'unchecked-assumption'
|
|
52
|
+
| 'missing-verify'
|
|
53
|
+
| 'ambiguous-acceptance'
|
|
54
|
+
| 'contradictory'
|
|
55
|
+
| 'product-semantics'
|
|
56
|
+
| 'typed-op'
|
|
57
|
+
| 'spend';
|
|
58
|
+
|
|
59
|
+
export interface Finding {
|
|
60
|
+
readonly id: string;
|
|
61
|
+
readonly line: number;
|
|
62
|
+
readonly kind: Kind;
|
|
63
|
+
readonly ownership: string;
|
|
64
|
+
readonly excerpt: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Kind → ownership class. The two owner-routed rows are the whole cost of
|
|
69
|
+
* closure: everything else is resolved without reaching a person, which is
|
|
70
|
+
* what "zero owner questions for twelve technical ambiguities" means.
|
|
71
|
+
*/
|
|
72
|
+
export const OWNERSHIP_BY_KIND: Readonly<Record<Kind, string>> = {
|
|
73
|
+
tbd: 'contested-technical',
|
|
74
|
+
'unpicked-alternative': 'contested-technical',
|
|
75
|
+
'unchecked-assumption': 'reversible-technical',
|
|
76
|
+
'missing-verify': 'deterministic',
|
|
77
|
+
'ambiguous-acceptance': 'reversible-technical',
|
|
78
|
+
contradictory: 'contested-technical',
|
|
79
|
+
'product-semantics': 'product-owned',
|
|
80
|
+
'typed-op': 'destructive-owned',
|
|
81
|
+
spend: 'spend-exhaustion',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Classes that reach a person. Everything else closes without an ask. */
|
|
85
|
+
export const OWNER_ROUTED: ReadonlySet<string> = new Set([
|
|
86
|
+
'product-owned',
|
|
87
|
+
'business-owned',
|
|
88
|
+
'destructive-owned',
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
interface Pattern {
|
|
92
|
+
readonly kind: Kind;
|
|
93
|
+
readonly re: RegExp;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const LINE_PATTERNS: readonly Pattern[] = [
|
|
97
|
+
{ kind: 'tbd', re: /\bTBD\b/ },
|
|
98
|
+
{ kind: 'tbd', re: /\bto be decided\b/i },
|
|
99
|
+
{ kind: 'tbd', re: /\bdecide later\b/i },
|
|
100
|
+
{ kind: 'unpicked-alternative', re: /\beither\b[^.]{3,80}\bor\b/i },
|
|
101
|
+
{ kind: 'unpicked-alternative', re: /\boption A\b[^.]{0,120}\boption B\b/i },
|
|
102
|
+
{ kind: 'unpicked-alternative', re: /\bwe could (?:either )?\w+[^.]{0,80}\bor\b/i },
|
|
103
|
+
// "two equal technical strategies" is the canonical unpicked alternative,
|
|
104
|
+
// and the phrasing an author reaches for when the choice is genuinely open
|
|
105
|
+
// — it carries no `either`, no `or` and no `option A`.
|
|
106
|
+
{ kind: 'unpicked-alternative', re: /\btwo (?:equal|competing)\b[^.]{0,60}\b(?:strateg|approach|design|option)/i },
|
|
107
|
+
{ kind: 'unchecked-assumption', re: /\bassuming\b/i },
|
|
108
|
+
{ kind: 'unchecked-assumption', re: /\bpresumably\b/i },
|
|
109
|
+
{ kind: 'unchecked-assumption', re: /\bprobably (?:fine|works|ok)\b/i },
|
|
110
|
+
{ kind: 'ambiguous-acceptance', re: /\b(?:improve|optimi[sz]e)\b(?![^.]*\bby\b)/i },
|
|
111
|
+
{ kind: 'ambiguous-acceptance', re: /\bmake it (?:better|cleaner|nicer)\b/i },
|
|
112
|
+
{ kind: 'ambiguous-acceptance', re: /\buse best practices\b/i },
|
|
113
|
+
{ kind: 'ambiguous-acceptance', re: /\bhandle errors properly\b/i },
|
|
114
|
+
{ kind: 'contradictory', re: /\bcontradicts\b/i },
|
|
115
|
+
{ kind: 'contradictory', re: /\bconflicts with\b/i },
|
|
116
|
+
{ kind: 'product-semantics', re: /\buser-visible\b[^.]{0,80}\b(?:or|versus|vs\.?)\b/i },
|
|
117
|
+
{ kind: 'product-semantics', re: /\bwhat the user sees\b/i },
|
|
118
|
+
{ kind: 'product-semantics', re: /\btwo valid (?:user|product) semantics\b/i },
|
|
119
|
+
{ kind: 'typed-op', re: /\b(?:merge|force-push|deploy|publish|purchase|rotate the secret)\b/i },
|
|
120
|
+
{ kind: 'spend', re: /\b(?:over|above|crosses?) the (?:configured )?(?:spend )?ceiling\b/i },
|
|
121
|
+
{ kind: 'spend', re: /\bquota (?:is )?(?:exhausted|gone)\b/i },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const STEP_RE = /^\s*-\s\[[ x~-]\]\s/;
|
|
125
|
+
const OPEN_STEP_RE = /^\s*-\s\[ \]\s/;
|
|
126
|
+
const H2_RE = /^##\s+/;
|
|
127
|
+
const DISCHARGE_H2_RE = /^##\s+(Decisions|Blockers|Kill register|Risk Register)\s*$/;
|
|
128
|
+
const ACCEPTANCE_H2_RE = /^##\s+Acceptance Criteria\s*$/;
|
|
129
|
+
|
|
130
|
+
function _isFence(line: string): boolean {
|
|
131
|
+
return /^\s*(```|~~~)/.test(line);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A scan unit: one logical statement, which is NOT one physical line.
|
|
136
|
+
*
|
|
137
|
+
* A roadmap step wraps over several lines, and the ambiguity it carries
|
|
138
|
+
* routinely straddles the wrap ("either newline-delimited JSON / or a single
|
|
139
|
+
* array"). Matching per line missed exactly those, which are the ones an
|
|
140
|
+
* author is least likely to notice. A step block is therefore ONE unit, joined
|
|
141
|
+
* and matched once — which also means one finding per step at most, so a step
|
|
142
|
+
* carrying two hedges is one open decision rather than two.
|
|
143
|
+
*/
|
|
144
|
+
export interface Unit {
|
|
145
|
+
/** 1-based line of the unit's first line. */
|
|
146
|
+
readonly line: number;
|
|
147
|
+
readonly text: string;
|
|
148
|
+
readonly inAcceptance: boolean;
|
|
149
|
+
readonly isOpenStep: boolean;
|
|
150
|
+
readonly hasVerify: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function units(lines: readonly string[]): Unit[] {
|
|
154
|
+
const out: Unit[] = [];
|
|
155
|
+
let inFence = false;
|
|
156
|
+
let inDischarge = false;
|
|
157
|
+
let inAcceptance = false;
|
|
158
|
+
let i = 0;
|
|
159
|
+
while (i < lines.length) {
|
|
160
|
+
const line = lines[i] as string;
|
|
161
|
+
if (_isFence(line)) {
|
|
162
|
+
inFence = !inFence;
|
|
163
|
+
i += 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (inFence) {
|
|
167
|
+
i += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (H2_RE.test(line)) {
|
|
171
|
+
inDischarge = DISCHARGE_H2_RE.test(line);
|
|
172
|
+
inAcceptance = ACCEPTANCE_H2_RE.test(line);
|
|
173
|
+
}
|
|
174
|
+
if (inDischarge) {
|
|
175
|
+
i += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (STEP_RE.test(line)) {
|
|
179
|
+
let j = i + 1;
|
|
180
|
+
while (j < lines.length && !STEP_RE.test(lines[j] as string) && !H2_RE.test(lines[j] as string)) {
|
|
181
|
+
j += 1;
|
|
182
|
+
}
|
|
183
|
+
const block = lines.slice(i, j);
|
|
184
|
+
out.push({
|
|
185
|
+
line: i + 1,
|
|
186
|
+
// Whitespace-normalised: a wrapped step joined with a single
|
|
187
|
+
// space still carries the next line's indent, so a pattern
|
|
188
|
+
// written with single spaces stops matching across the wrap —
|
|
189
|
+
// which is the case this unit exists to catch.
|
|
190
|
+
text: block.join(' ').replace(/\s+/g, ' '),
|
|
191
|
+
inAcceptance,
|
|
192
|
+
isOpenStep: OPEN_STEP_RE.test(line),
|
|
193
|
+
hasVerify: /(?:^|\s)verify:/m.test(block.join('\n')),
|
|
194
|
+
});
|
|
195
|
+
i = j;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
out.push({
|
|
199
|
+
line: i + 1,
|
|
200
|
+
text: line,
|
|
201
|
+
inAcceptance,
|
|
202
|
+
isOpenStep: false,
|
|
203
|
+
hasVerify: false,
|
|
204
|
+
});
|
|
205
|
+
i += 1;
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function scan(text: string): Finding[] {
|
|
211
|
+
const lines = text.split('\n');
|
|
212
|
+
const findings: Finding[] = [];
|
|
213
|
+
|
|
214
|
+
for (const u of units(lines)) {
|
|
215
|
+
if (/<!--\s*closure:\s*ignore\s*-->/.test(u.text)) continue;
|
|
216
|
+
let matched: Kind | null = null;
|
|
217
|
+
for (const { kind, re } of LINE_PATTERNS) {
|
|
218
|
+
if (re.test(u.text)) {
|
|
219
|
+
matched = kind;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// An open step carries a `verify:` line — a step whose success cannot
|
|
224
|
+
// be checked is a trigger for a question, not for work. Acceptance
|
|
225
|
+
// criteria are criteria, not steps: they have their own contract and
|
|
226
|
+
// carry no verify line by design.
|
|
227
|
+
if (matched === null && u.isOpenStep && !u.hasVerify && !u.inAcceptance) {
|
|
228
|
+
matched = 'missing-verify';
|
|
229
|
+
}
|
|
230
|
+
if (matched === null) continue;
|
|
231
|
+
findings.push({
|
|
232
|
+
id: '',
|
|
233
|
+
line: u.line,
|
|
234
|
+
kind: matched,
|
|
235
|
+
ownership: OWNERSHIP_BY_KIND[matched],
|
|
236
|
+
excerpt: u.text.trim().replace(/\s+/g, ' ').slice(0, 120),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
findings.sort((a, b) => a.line - b.line);
|
|
241
|
+
return findings.map((f, idx) => ({ ...f, id: `C${String(idx + 1)}` }));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The `## Decisions` skeleton — one row per finding, resolution left open. */
|
|
245
|
+
export function renderRows(findings: readonly Finding[]): string {
|
|
246
|
+
const header = '| ID | ownership | resolved by | decision | evidence | revisit if |';
|
|
247
|
+
const sep = '|---|---|---|---|---|---|';
|
|
248
|
+
const rows = findings.map(
|
|
249
|
+
(f) => `| ${f.id} | ${f.ownership} | <resolver> | <decision> | ${f.kind}:${String(f.line)} | <condition> |`,
|
|
250
|
+
);
|
|
251
|
+
return [header, sep, ...rows].join('\n');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function ownerQuestionCount(findings: readonly Finding[]): number {
|
|
255
|
+
return findings.filter((f) => OWNER_ROUTED.has(f.ownership)).length;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --------------------------------------------------------------------------
|
|
259
|
+
// CLI.
|
|
260
|
+
// --------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
function _usage(): string {
|
|
263
|
+
return (
|
|
264
|
+
`usage: ${_PROG} [-h] [--json] [--rows] [--strict] <roadmap.md>\n\n` +
|
|
265
|
+
`Find and classify the open decisions in a plan. Resolves nothing.\n`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function main(argv?: readonly string[]): number {
|
|
270
|
+
const args = argv ?? process.argv.slice(2);
|
|
271
|
+
let json = false;
|
|
272
|
+
let rows = false;
|
|
273
|
+
let strict = false;
|
|
274
|
+
const paths: string[] = [];
|
|
275
|
+
for (const a of args) {
|
|
276
|
+
if (a === '-h' || a === '--help') {
|
|
277
|
+
process.stdout.write(_usage());
|
|
278
|
+
return 0;
|
|
279
|
+
} else if (a === '--json') json = true;
|
|
280
|
+
else if (a === '--rows') rows = true;
|
|
281
|
+
else if (a === '--strict') strict = true;
|
|
282
|
+
else if (a.startsWith('-')) {
|
|
283
|
+
process.stderr.write(`${_usage()}${_PROG}: error: unrecognized arguments: ${a}\n`);
|
|
284
|
+
return 2;
|
|
285
|
+
} else paths.push(a);
|
|
286
|
+
}
|
|
287
|
+
if (paths.length !== 1) {
|
|
288
|
+
process.stderr.write(`${_usage()}${_PROG}: error: exactly one roadmap path is required\n`);
|
|
289
|
+
return 2;
|
|
290
|
+
}
|
|
291
|
+
const target = path.resolve(paths[0] as string);
|
|
292
|
+
let text: string;
|
|
293
|
+
try {
|
|
294
|
+
text = fs.readFileSync(target, 'utf-8');
|
|
295
|
+
} catch {
|
|
296
|
+
process.stderr.write(`${_PROG}: error: cannot read ${paths[0] as string}\n`);
|
|
297
|
+
return 2;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const findings = scan(text);
|
|
301
|
+
const owner = ownerQuestionCount(findings);
|
|
302
|
+
|
|
303
|
+
if (json) {
|
|
304
|
+
process.stdout.write(
|
|
305
|
+
JSON.stringify({ findings, open: findings.length, owner_questions: owner }, null, 2) + '\n',
|
|
306
|
+
);
|
|
307
|
+
} else if (rows) {
|
|
308
|
+
process.stdout.write(renderRows(findings) + '\n');
|
|
309
|
+
} else {
|
|
310
|
+
process.stdout.write(`open decisions: ${String(findings.length)}\n`);
|
|
311
|
+
process.stdout.write(`owner questions: ${String(owner)}\n`);
|
|
312
|
+
for (const f of findings) {
|
|
313
|
+
process.stdout.write(
|
|
314
|
+
` ${f.id} ${String(f.line).padStart(4)} ${f.kind.padEnd(20)} ${f.ownership.padEnd(22)} ${f.excerpt}\n`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!findings.length) return 0;
|
|
319
|
+
return strict ? 1 : 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function _isCliEntry(): boolean {
|
|
323
|
+
if (!process.argv[1]) return false;
|
|
324
|
+
try {
|
|
325
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
326
|
+
} catch {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
332
|
+
process.exitCode = main();
|
|
333
|
+
}
|
|
@@ -23,10 +23,21 @@
|
|
|
23
23
|
* Run after editing the manifest:
|
|
24
24
|
* ./scripts-run src/scripts/compile_hook_manifest
|
|
25
25
|
*
|
|
26
|
+
* `--out <path>` writes the compiled bytes THERE and leaves the tree alone. It
|
|
27
|
+
* exists for `check_generator_sync`, whose contract is that a triple regenerates
|
|
28
|
+
* into a temp dir the gate owns and never into the working tree — a gate that
|
|
29
|
+
* repaired the artefact it is measuring would turn every staleness into a silent
|
|
30
|
+
* green plus an unexplained dirty file. Anything that wants to compare against
|
|
31
|
+
* the committed JSON uses this flag; anything that wants to FIX it omits it.
|
|
32
|
+
*
|
|
26
33
|
* `tests/hooks/hook_manifest_compiled.test.ts` fails if the committed JSON
|
|
27
|
-
* stops matching the YAML, so a forgotten run is caught rather than shipped
|
|
34
|
+
* stops matching the YAML, so a forgotten run is caught rather than shipped —
|
|
35
|
+
* but only against the COMMITTED blob in CI. Locally that same test reads the
|
|
36
|
+
* working tree, so a recompile that was run and never committed makes it pass
|
|
37
|
+
* over a defect that is still on the branch. See the discriminator in
|
|
38
|
+
* `check_generator_sync`'s header.
|
|
28
39
|
*
|
|
29
|
-
* Exit codes: 0 written (or already current) · 2 internal error.
|
|
40
|
+
* Exit codes: 0 written (or already current) · 2 internal error or usage.
|
|
30
41
|
*/
|
|
31
42
|
import * as fs from 'node:fs';
|
|
32
43
|
import * as path from 'node:path';
|
|
@@ -46,9 +57,38 @@ export function compile(yamlText: string): string {
|
|
|
46
57
|
});
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
export function main(): number {
|
|
60
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
61
|
+
let outPath: string | null = null;
|
|
62
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
63
|
+
const a = argv[i] as string;
|
|
64
|
+
if (a === '--out') {
|
|
65
|
+
const next = argv[i + 1];
|
|
66
|
+
if (next === undefined || next.startsWith('--')) {
|
|
67
|
+
process.stderr.write('compile_hook_manifest: --out needs a path\n');
|
|
68
|
+
return 2;
|
|
69
|
+
}
|
|
70
|
+
outPath = next;
|
|
71
|
+
i += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
process.stderr.write(
|
|
75
|
+
`compile_hook_manifest: unknown argument ${JSON.stringify(a)}\n` +
|
|
76
|
+
'usage: compile_hook_manifest [--out <path>]\n',
|
|
77
|
+
);
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
|
|
50
81
|
const text = fs.readFileSync(YAML_PATH, 'utf-8');
|
|
51
82
|
const next = compile(text);
|
|
83
|
+
|
|
84
|
+
if (outPath !== null) {
|
|
85
|
+
// Comparison copy. No `already current` short-circuit: the caller asked
|
|
86
|
+
// for the bytes, and an unwritten file would read as a generator that
|
|
87
|
+
// silently did nothing.
|
|
88
|
+
fs.writeFileSync(outPath, next);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
52
92
|
const current = fs.existsSync(JSON_PATH) ? fs.readFileSync(JSON_PATH, 'utf-8') : null;
|
|
53
93
|
if (current === next) {
|
|
54
94
|
process.stdout.write('compile_hook_manifest: already current\n');
|