@event4u/agent-config 12.0.0 → 13.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 +273 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +46 -3
- package/dist/agent-src/commands/cost/report.md +23 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
- package/dist/agent-src/contexts/model-recommendations.md +6 -4
- 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/design-fidelity.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +10 -5
- 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/low-impact-corpus-privacy-floor.md +1 -0
- package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
- package/dist/agent-src/rules/ui-audit-gate.md +9 -4
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +30 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -1
- 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/merge-conflicts/SKILL.md +4 -0
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +11 -11
- 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 +41 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-NN4KW4HZ.js → chunk-3B72LSZL.js} +123 -2338
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/chunk-N7FEST6D.js +3895 -0
- package/dist/cli-delegate/{chunk-L565YFXH.js → chunk-XURJ4AQC.js} +566 -298
- package/dist/cli-delegate/cmd_conformance.js +4 -4
- 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 +44 -23
- 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 +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +10625 -8541
- package/dist/install/install.mjs +754 -484
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1302 -167
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/model_tier.js +9 -0
- package/dist/scripts/_lib/model_tier.js.map +1 -1
- package/dist/scripts/condense.js +50 -0
- 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/docs/MIGRATION.md +20 -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/roadmap_gates.ts +30 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -1
- package/src/config/evaluator-budgets.json +9 -8
- package/src/config/gate-coverage.yml +63 -11
- package/src/config/gate-violation-baselines.json +12 -0
- package/src/config/hook-token-budget.json +15 -2
- package/src/config/preamble-payload-budget.json +28 -0
- package/src/scripts/_dispatch.bash +10 -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/git_common_dir.ts +173 -0
- package/src/scripts/_lib/model_tier.ts +9 -0
- package/src/scripts/_lib/release_highlights.ts +108 -7
- package/src/scripts/_lib/skill_catalogue.ts +1115 -0
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- package/src/scripts/ai_council/clients.ts +197 -18
- package/src/scripts/ai_council/config.ts +16 -1
- package/src/scripts/ai_council/low_impact.ts +14 -1
- package/src/scripts/ai_council/orchestrator.ts +14 -1
- package/src/scripts/ai_council/transport_resolver.ts +32 -2
- 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 +279 -317
- package/src/scripts/check_branch_freshness.ts +7 -1
- package/src/scripts/check_release_published.ts +6 -2
- package/src/scripts/check_release_trunk_sync.ts +18 -2
- package/src/scripts/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -1
- package/src/scripts/condense.ts +50 -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/dispatch_r2_reviewer.ts +15 -5
- package/src/scripts/evidence_report.ts +15 -4
- package/src/scripts/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +75 -15
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +21 -0
- package/src/scripts/hooks/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/tool_result_bytes_hook.ts +219 -0
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/hot_context_hook.ts +13 -1
- package/src/scripts/install.ts +129 -0
- package/src/scripts/janitor.ts +77 -2
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_plan_risk_register.ts +22 -9
- package/src/scripts/lint_roadmap_blockers.ts +109 -6
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_scheduled_deprecations.ts +738 -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/migration_status.ts +17 -1
- package/src/scripts/probe_review_binding_drift.ts +731 -0
- package/src/scripts/release.ts +100 -6
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/schemas/command.schema.json +3 -3
- package/src/scripts/schemas/skill.schema.json +20 -2
- package/src/scripts/schemas/subagent.schema.json +2 -2
- package/src/scripts/session_eol_report.ts +81 -0
- package/src/scripts/trigger_coverage.ts +224 -3
- package/src/scripts/workspace_doctor.ts +464 -0
- package/src/scripts/worktree_cleanup_check.ts +2 -1
- package/src/server/schemas/settings.ts +2 -2
- package/dist/cli-delegate/chunk-AYKQGIBG.js +0 -1594
- package/src/config/budget-routing.json +0 -7
package/src/scripts/install.ts
CHANGED
|
@@ -81,6 +81,12 @@ import * as rule_layer_overlap from './_lib/rule_layer_overlap.js';
|
|
|
81
81
|
import * as installed_tools from './_lib/installed_tools.js';
|
|
82
82
|
import { collect_drift, format_drift_report } from './_lib/install_drift.js';
|
|
83
83
|
import * as user_global_paths from './_lib/user_global_paths.js';
|
|
84
|
+
// The LIBRARY half, deliberately not `capture_skill_catalogue.js`: that module
|
|
85
|
+
// is a CLI entry with a top-level `process.exit()`, and esbuild would bundle
|
|
86
|
+
// that exit into the installer a consumer loads (`check_installer_import_purity`
|
|
87
|
+
// catches exactly this). The lib has no entry and no exit.
|
|
88
|
+
import * as catalogue from './_lib/skill_catalogue.js';
|
|
89
|
+
import { isInteractiveSession, type SessionProbe } from '../shared/interactiveContext.js';
|
|
84
90
|
import * as claude_desktop_bundler from './_lib/claude_desktop_bundler.js';
|
|
85
91
|
import * as claude_settings_hooks from './_lib/claude_settings_hooks.js';
|
|
86
92
|
import { find_project_root_with_anchor, load_agent_settings } from './_lib/agent_settings.js';
|
|
@@ -3899,6 +3905,32 @@ function install_global(
|
|
|
3899
3905
|
if (!state.QUIET) warn(`settings-surface snapshot failed — ${String(e)}`);
|
|
3900
3906
|
}
|
|
3901
3907
|
|
|
3908
|
+
// Catalogue-budget warning (road-to-skill-catalogue-budget Phase 1).
|
|
3909
|
+
// Some hosts silently truncate the skill catalogue they were handed: the
|
|
3910
|
+
// deploy reports success, and the model never sees most of what was
|
|
3911
|
+
// deployed. A host that publishes its own dropped count has a MEASURED
|
|
3912
|
+
// ceiling; one that does not gets no warning and no invented number.
|
|
3913
|
+
// Warn only — over-shipping is the safe direction, and a consumer who
|
|
3914
|
+
// accepts truncation is making a legitimate choice.
|
|
3915
|
+
if (!state.QUIET) {
|
|
3916
|
+
try {
|
|
3917
|
+
for (const line of _catalogue_truncation_warnings(deploy_results, project_root)) {
|
|
3918
|
+
warn(line);
|
|
3919
|
+
}
|
|
3920
|
+
const notice = _scoped_migration_notice(deploy_results, project_root, package_root, {
|
|
3921
|
+
env: process.env,
|
|
3922
|
+
stdinTty: process.stdin.isTTY === true,
|
|
3923
|
+
stdoutTty: process.stdout.isTTY === true,
|
|
3924
|
+
});
|
|
3925
|
+
if (notice.length > 0) {
|
|
3926
|
+
process.stdout.write('\n');
|
|
3927
|
+
for (const line of notice) info(line);
|
|
3928
|
+
}
|
|
3929
|
+
} catch {
|
|
3930
|
+
// A measurement must never sink an install that otherwise worked.
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3902
3934
|
if (!state.QUIET) {
|
|
3903
3935
|
process.stdout.write('\n');
|
|
3904
3936
|
success(`Global install completed (v${installed_version}).`);
|
|
@@ -3907,6 +3939,103 @@ function install_global(
|
|
|
3907
3939
|
return 0;
|
|
3908
3940
|
}
|
|
3909
3941
|
|
|
3942
|
+
/**
|
|
3943
|
+
* One line per deployed host whose catalogue exceeds a ceiling this machine has
|
|
3944
|
+
* actually measured. Empty when nothing has been measured — the observation log
|
|
3945
|
+
* lives where captures accumulate, and an install on a machine that never ran
|
|
3946
|
+
* one has no basis to claim a limit.
|
|
3947
|
+
*
|
|
3948
|
+
* Returns lines rather than printing them so the decision is testable without
|
|
3949
|
+
* capturing stdout.
|
|
3950
|
+
*/
|
|
3951
|
+
export function _catalogue_truncation_warnings(
|
|
3952
|
+
deploy_results: Record<string, DeployResult>,
|
|
3953
|
+
project_root: string | null,
|
|
3954
|
+
): string[] {
|
|
3955
|
+
const log_candidates = [
|
|
3956
|
+
path.join(user_global_paths.event4u_root(), 'state', 'skill-catalogue.jsonl'),
|
|
3957
|
+
...(project_root ? [path.join(project_root, catalogue.OBSERVATION_LOG)] : []),
|
|
3958
|
+
];
|
|
3959
|
+
const records = log_candidates.flatMap((p) => catalogue.readObservationLog(p));
|
|
3960
|
+
if (records.length === 0) return [];
|
|
3961
|
+
|
|
3962
|
+
const limits = catalogue.knownHostLimits(records);
|
|
3963
|
+
if (limits.size === 0) return [];
|
|
3964
|
+
|
|
3965
|
+
const lines: string[] = [];
|
|
3966
|
+
for (const tool_id of Object.keys(deploy_results).sort()) {
|
|
3967
|
+
const [, , status] = deploy_results[tool_id] as DeployResult;
|
|
3968
|
+
if (status !== 'deployed') continue;
|
|
3969
|
+
const limit = limits.get(tool_id);
|
|
3970
|
+
if (limit === undefined) continue;
|
|
3971
|
+
const anchor_raw = USER_SCOPE_PATHS[tool_id];
|
|
3972
|
+
if (!anchor_raw) continue;
|
|
3973
|
+
|
|
3974
|
+
const volume = catalogue.measureCatalogueVolume(tool_id, expanduser(anchor_raw));
|
|
3975
|
+
const warning = catalogue.catalogueLimitWarning(volume, limit);
|
|
3976
|
+
if (warning !== null) lines.push(warning);
|
|
3977
|
+
}
|
|
3978
|
+
return lines;
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
/**
|
|
3982
|
+
* The scoped-projection migration notice for an EXISTING install.
|
|
3983
|
+
*
|
|
3984
|
+
* Shape decided by AI council 2026-08-15 (2/2 converged, both members present):
|
|
3985
|
+
* option B — the existing warning PLUS an interactive notice that persists
|
|
3986
|
+
* NOTHING and names the manual edit and the GUI route. `projection.mode` is
|
|
3987
|
+
* settings class C, so no agent may write it; a notice that offered a CLI
|
|
3988
|
+
* write would send the reader to a command guaranteed to refuse them.
|
|
3989
|
+
*
|
|
3990
|
+
* Eligibility and delivery are separate on purpose. A non-interactive run can
|
|
3991
|
+
* be fully eligible; it simply gets the warning and no notice, and the two
|
|
3992
|
+
* outcomes stay distinguishable instead of collapsing into "nothing printed".
|
|
3993
|
+
*
|
|
3994
|
+
* Known and accepted: with nothing persisted, a qualifying install sees this
|
|
3995
|
+
* on every deploy. The council named that as the strongest argument for doing
|
|
3996
|
+
* nothing at all, and it does not outweigh making a measured, silent
|
|
3997
|
+
* capability loss an explicit human decision.
|
|
3998
|
+
*/
|
|
3999
|
+
export function _scoped_migration_notice(
|
|
4000
|
+
deploy_results: Record<string, DeployResult>,
|
|
4001
|
+
project_root: string | null,
|
|
4002
|
+
package_root: string,
|
|
4003
|
+
probe: SessionProbe,
|
|
4004
|
+
): string[] {
|
|
4005
|
+
if (!isInteractiveSession(probe)) return [];
|
|
4006
|
+
|
|
4007
|
+
const log_candidates = [
|
|
4008
|
+
path.join(user_global_paths.event4u_root(), 'state', 'skill-catalogue.jsonl'),
|
|
4009
|
+
...(project_root ? [path.join(project_root, catalogue.OBSERVATION_LOG)] : []),
|
|
4010
|
+
];
|
|
4011
|
+
const limits = catalogue.knownHostLimits(log_candidates.flatMap((p) => catalogue.readObservationLog(p)));
|
|
4012
|
+
if (limits.size === 0) return [];
|
|
4013
|
+
|
|
4014
|
+
const resolved = _resolve_scoped_projection(package_root);
|
|
4015
|
+
const settings_path =
|
|
4016
|
+
_resolve_global_settings_path() ??
|
|
4017
|
+
path.join(user_global_paths.event4u_root(), 'settings', SETTINGS_FILE);
|
|
4018
|
+
|
|
4019
|
+
for (const tool_id of Object.keys(deploy_results).sort()) {
|
|
4020
|
+
const [, , status] = deploy_results[tool_id] as DeployResult;
|
|
4021
|
+
if (status !== 'deployed') continue;
|
|
4022
|
+
const anchor_raw = USER_SCOPE_PATHS[tool_id];
|
|
4023
|
+
if (!anchor_raw) continue;
|
|
4024
|
+
|
|
4025
|
+
const volume = catalogue.measureCatalogueVolume(tool_id, expanduser(anchor_raw));
|
|
4026
|
+
const eligibility = catalogue.migrationEligibility(
|
|
4027
|
+
tool_id,
|
|
4028
|
+
resolved.mode,
|
|
4029
|
+
volume.skillEntries,
|
|
4030
|
+
limits,
|
|
4031
|
+
);
|
|
4032
|
+
if (eligibility.eligible) {
|
|
4033
|
+
return catalogue.migrationPromptLines(tool_id, eligibility, settings_path);
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
return [];
|
|
4037
|
+
}
|
|
4038
|
+
|
|
3910
4039
|
const SETTINGS_SURFACE_REL = path.join('state', 'settings-surface.json');
|
|
3911
4040
|
const SETTINGS_DELTA_REL = path.join('state', 'settings-delta.json');
|
|
3912
4041
|
|
package/src/scripts/janitor.ts
CHANGED
|
@@ -24,22 +24,82 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
24
24
|
const _HERE = fileURLToPath(import.meta.url);
|
|
25
25
|
const REPO_ROOT = path.join(path.dirname(_HERE), '..', '..');
|
|
26
26
|
|
|
27
|
-
// TTL configuration per directory (days)
|
|
28
|
-
|
|
27
|
+
// TTL configuration per directory (days).
|
|
28
|
+
//
|
|
29
|
+
// `owner` / `reviewBy` (road-to-inbox-harvest-2026-08-d-context-ledger Step
|
|
30
|
+
// 4.1) put every TTL under the same discipline the two budget config files
|
|
31
|
+
// carry: a retention number nobody owns and nobody revisits is a guess that
|
|
32
|
+
// hardens into a fact. The falsifier is printed per run — see the reclaim
|
|
33
|
+
// total in `main` — so a TTL that reclaims nothing over a review window is
|
|
34
|
+
// removed rather than defended.
|
|
35
|
+
const TTL_CONFIG: ReadonlyArray<{
|
|
36
|
+
dir: string;
|
|
37
|
+
ttlDays: number;
|
|
38
|
+
description: string;
|
|
39
|
+
owner: string;
|
|
40
|
+
reviewBy: string;
|
|
41
|
+
}> = [
|
|
29
42
|
{
|
|
30
43
|
dir: 'agents/tmp.old',
|
|
31
44
|
ttlDays: 30,
|
|
32
45
|
description: 'Processed inbox archive (consumed agents/tmp/ files)',
|
|
46
|
+
owner: 'maintainer',
|
|
47
|
+
reviewBy: '2026-11-10',
|
|
33
48
|
},
|
|
34
49
|
{
|
|
35
50
|
dir: 'agents/runtime/tmp',
|
|
36
51
|
ttlDays: 7,
|
|
37
52
|
description: 'Agent scratch / runtime tmp',
|
|
53
|
+
owner: 'maintainer',
|
|
54
|
+
reviewBy: '2026-11-10',
|
|
38
55
|
},
|
|
39
56
|
{
|
|
40
57
|
dir: 'agents/runtime/council/responses',
|
|
41
58
|
ttlDays: 7,
|
|
42
59
|
description: 'Council response cache',
|
|
60
|
+
owner: 'maintainer',
|
|
61
|
+
reviewBy: '2026-11-10',
|
|
62
|
+
},
|
|
63
|
+
// ── Session-scoped runtime state (Step 4.1) ────────────────────────────
|
|
64
|
+
//
|
|
65
|
+
// These four directories accrete ONE entry per session behind a
|
|
66
|
+
// once-per-session latch, and nothing in the tree removed them: writers
|
|
67
|
+
// and readers existed, no reaper did. They are named INDIVIDUALLY rather
|
|
68
|
+
// than sweeping `agents/runtime/state` as a whole, and that is the
|
|
69
|
+
// load-bearing decision in this step.
|
|
70
|
+
//
|
|
71
|
+
// `agents/runtime/state` also holds the CUMULATIVE measurement streams —
|
|
72
|
+
// `audit/`, `injection-census.jsonl`, `tool-result-census.jsonl`,
|
|
73
|
+
// `ai-video-telemetry.jsonl`, the memory index — and a directory-level
|
|
74
|
+
// TTL would delete exactly the corpora this roadmap exists to build. A
|
|
75
|
+
// sweeper that eats the instrument's own data is worse than no sweeper.
|
|
76
|
+
{
|
|
77
|
+
dir: 'agents/runtime/state/session-eol',
|
|
78
|
+
ttlDays: 7,
|
|
79
|
+
description: 'Per-session end-of-life counters (one file per session)',
|
|
80
|
+
owner: 'maintainer',
|
|
81
|
+
reviewBy: '2026-11-10',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
dir: 'agents/runtime/state/reread-guard',
|
|
85
|
+
ttlDays: 7,
|
|
86
|
+
description: 'Per-session re-read latch',
|
|
87
|
+
owner: 'maintainer',
|
|
88
|
+
reviewBy: '2026-11-10',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
dir: 'agents/runtime/state/end-review-nudge',
|
|
92
|
+
ttlDays: 7,
|
|
93
|
+
description: 'Per-session end-review latch',
|
|
94
|
+
owner: 'maintainer',
|
|
95
|
+
reviewBy: '2026-11-10',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
dir: 'agents/runtime/state/probe-throttle',
|
|
99
|
+
ttlDays: 7,
|
|
100
|
+
description: 'Per-session probe throttle latch',
|
|
101
|
+
owner: 'maintainer',
|
|
102
|
+
reviewBy: '2026-11-10',
|
|
43
103
|
},
|
|
44
104
|
];
|
|
45
105
|
|
|
@@ -227,6 +287,21 @@ function report(apply: boolean): void {
|
|
|
227
287
|
);
|
|
228
288
|
} else if (apply && totalExpired > 0) {
|
|
229
289
|
process.stdout.write(`Total deleted: ${totalExpired} file(s), ${_humanSize(totalSize)} freed.\n`);
|
|
290
|
+
} else {
|
|
291
|
+
// road-to-inbox-harvest-2026-08-d-context-ledger Step 4.2 — the TTL's
|
|
292
|
+
// own falsifier, and it only works if a ZERO is printed.
|
|
293
|
+
//
|
|
294
|
+
// Until now a run that reclaimed nothing printed no total at all, so
|
|
295
|
+
// "the sweeper ran and found nothing expired" and "the sweeper did not
|
|
296
|
+
// run" produced byte-identical output. That is the difference between
|
|
297
|
+
// evidence that a retention policy is unnecessary and no evidence at
|
|
298
|
+
// all — and the first is what removes a TTL entry rather than letting
|
|
299
|
+
// it harden into an unexamined default.
|
|
300
|
+
process.stdout.write(
|
|
301
|
+
`Total ${apply ? 'deleted' : 'reclaimable'}: 0 file(s), 0 B — nothing was past its TTL.\n` +
|
|
302
|
+
`A reclaim that stays at zero across a review window is the signal to drop the ` +
|
|
303
|
+
`TTL entry, not to keep it: see the owner / reviewBy fields on each entry.\n`,
|
|
304
|
+
);
|
|
230
305
|
}
|
|
231
306
|
}
|
|
232
307
|
|
|
@@ -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`);
|
|
@@ -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) {
|
|
@@ -39,6 +39,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
39
39
|
|
|
40
40
|
import { parse as parseYaml } from 'yaml';
|
|
41
41
|
|
|
42
|
+
import { workspaceIdentity } from './_lib/git_common_dir.js';
|
|
42
43
|
import { gitEnv } from './_lib/git_env.js';
|
|
43
44
|
import { splitMarkdownRow } from './_lib/md_table.js';
|
|
44
45
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
@@ -505,12 +506,26 @@ interface HistoryEntry {
|
|
|
505
506
|
date: string; // %cs — YYYY-MM-DD
|
|
506
507
|
}
|
|
507
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Repo root containing `dir`, or `null` outside a repository.
|
|
511
|
+
*
|
|
512
|
+
* Census rows R5–R7. The three call sites below each spawned
|
|
513
|
+
* `rev-parse --show-toplevel` through `_git` — already `GIT_DIR`-safe via
|
|
514
|
+
* `gitEnv()`, so this migration is behaviour-identical by construction and
|
|
515
|
+
* exists to remove the third, fourth and fifth re-derivation of one question,
|
|
516
|
+
* not to fix a defect. The resolver reads files instead of spawning, so the
|
|
517
|
+
* three checks also stop costing three subprocesses.
|
|
518
|
+
*/
|
|
519
|
+
function _repoRootOf(dir: string): string | null {
|
|
520
|
+
const root = workspaceIdentity(dir).repoRoot;
|
|
521
|
+
return root.resolved ? root.value : null;
|
|
522
|
+
}
|
|
523
|
+
|
|
508
524
|
/** Newest-first commit history of a file; empty for untracked / non-repo paths. */
|
|
509
525
|
export function fileHistory(absPath: string): HistoryEntry[] {
|
|
510
526
|
const dir = path.dirname(absPath);
|
|
511
|
-
const
|
|
512
|
-
if (
|
|
513
|
-
const root = top.trim();
|
|
527
|
+
const root = _repoRootOf(dir);
|
|
528
|
+
if (root === null) return [];
|
|
514
529
|
// Normalize separators: git pathspecs use `/` on every platform, so a raw
|
|
515
530
|
// Windows `path.relative()` result matches nothing and would silently yield
|
|
516
531
|
// an empty history — which the grandfather clause reads as "no baseline".
|
|
@@ -540,9 +555,8 @@ export function fileHistory(absPath: string): HistoryEntry[] {
|
|
|
540
555
|
*/
|
|
541
556
|
export function hasUncommittedChanges(absPath: string): boolean {
|
|
542
557
|
const dir = path.dirname(absPath);
|
|
543
|
-
const
|
|
544
|
-
if (
|
|
545
|
-
const root = top.trim();
|
|
558
|
+
const root = _repoRootOf(dir);
|
|
559
|
+
if (root === null) return false;
|
|
546
560
|
const rel = path.relative(root, absPath).split(path.sep).join('/');
|
|
547
561
|
const out = _git(root, ['status', '--porcelain', '--', rel]);
|
|
548
562
|
if (out === null) return false;
|
|
@@ -551,9 +565,8 @@ export function hasUncommittedChanges(absPath: string): boolean {
|
|
|
551
565
|
|
|
552
566
|
function _blobAt(absPath: string, sha: string): string | null {
|
|
553
567
|
const dir = path.dirname(absPath);
|
|
554
|
-
const
|
|
555
|
-
if (
|
|
556
|
-
const root = top.trim();
|
|
568
|
+
const root = _repoRootOf(dir);
|
|
569
|
+
if (root === null) return null;
|
|
557
570
|
const rel = path.relative(root, absPath).split(path.sep).join('/');
|
|
558
571
|
return _git(root, ['show', `${sha}:${rel}`]);
|
|
559
572
|
}
|
|
@@ -20,6 +20,7 @@ import * as fs from 'node:fs';
|
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
22
22
|
|
|
23
|
+
import { checkRatchet } from './_lib/gate_baseline.js';
|
|
23
24
|
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
24
25
|
|
|
25
26
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -46,6 +47,41 @@ const REQUIRED_FIELDS: ReadonlyArray<readonly [string, RegExp]> = [
|
|
|
46
47
|
['Resolved when', /^-[ \t]*\*\*Resolved when:\*\*/im],
|
|
47
48
|
];
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The fields that make a blocker DECIDABLE rather than merely described.
|
|
52
|
+
*
|
|
53
|
+
* Separated from `REQUIRED_FIELDS` on purpose: the five above are a settled
|
|
54
|
+
* contract every entry in the tree already satisfies, so a missing one is a
|
|
55
|
+
* hard failure. These three are new, and 36 open entries predate them — held
|
|
56
|
+
* to the same bar on day one they would red the whole backlog, which is the
|
|
57
|
+
* "strict gate fires on ~283 files" failure this repo has already recorded
|
|
58
|
+
* once. They run through the ratchet instead: the backlog is legal at its
|
|
59
|
+
* measured size, and every entry added or edited after this must clear them.
|
|
60
|
+
*/
|
|
61
|
+
const DECIDABILITY_FIELDS: ReadonlyArray<readonly [string, RegExp]> = [
|
|
62
|
+
['Recommendation', /^-[ \t]*\*\*Recommendation:\*\*[ \t]*\S/im],
|
|
63
|
+
['If you do nothing', /^-[ \t]*\*\*If you do nothing:\*\*[ \t]*\S/im],
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/** `What to do:` body — everything up to the next field marker. */
|
|
67
|
+
const WHAT_TO_DO_SLICE_RE =
|
|
68
|
+
/^-[ \t]*\*\*What to do:\*\*([\s\S]*?)(?=^-[ \t]*\*\*[A-Z]|^###[ \t]|$(?![\s\S]))/im;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Does `What to do` carry anything the owner can actually execute?
|
|
72
|
+
*
|
|
73
|
+
* A backticked span (a command, a path, a flag) or an enumerated option set
|
|
74
|
+
* (`(a)` / `1.`) both count. Bare prose does not: "pick exactly one — accept
|
|
75
|
+
* the reading, or re-cut the criterion" names no file, no command and no
|
|
76
|
+
* consequence, and hands the analysis back to the person least equipped to
|
|
77
|
+
* redo it. Measured 2026-08-15: 14 of 46 entries in this tree are that shape.
|
|
78
|
+
*/
|
|
79
|
+
function _hasExecutableSubstance(body: string): boolean {
|
|
80
|
+
const m = WHAT_TO_DO_SLICE_RE.exec(body);
|
|
81
|
+
const slice = m ? (m[1] as string) : '';
|
|
82
|
+
return /`[^`\n]+`/.test(slice) || /(^|\s)\((?:[a-z]|[0-9]+)\)\s/i.test(slice);
|
|
83
|
+
}
|
|
84
|
+
|
|
49
85
|
interface Violation {
|
|
50
86
|
line: number;
|
|
51
87
|
message: string;
|
|
@@ -68,7 +104,19 @@ function _stripFencedCode(text: string): string {
|
|
|
68
104
|
}
|
|
69
105
|
|
|
70
106
|
function _scan(rawText: string): Violation[] {
|
|
107
|
+
return _scanBoth(rawText).hard;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface ScanResult {
|
|
111
|
+
/** Contract violations — always fatal. */
|
|
112
|
+
hard: Violation[];
|
|
113
|
+
/** Open entries that are not yet decidable — counted against the ratchet. */
|
|
114
|
+
decidability: Violation[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _scanBoth(rawText: string): ScanResult {
|
|
71
118
|
const violations: Violation[] = [];
|
|
119
|
+
const decidability: Violation[] = [];
|
|
72
120
|
const text = _stripFencedCode(rawText);
|
|
73
121
|
const declaredIds = new Set<string>();
|
|
74
122
|
|
|
@@ -105,6 +153,23 @@ function _scan(rawText: string): Violation[] {
|
|
|
105
153
|
message: `blocker '${cur.id}' missing required field(s): ${missing.join(', ')}`,
|
|
106
154
|
});
|
|
107
155
|
}
|
|
156
|
+
// Resolved entries are history — re-litigating a decision already
|
|
157
|
+
// made would be churn, and the ratchet would never reach zero.
|
|
158
|
+
const isResolved = /^-[ \t]*\*\*Status:\*\*[ \t]*resolved/im.test(body);
|
|
159
|
+
if (!isResolved) {
|
|
160
|
+
const gaps = DECIDABILITY_FIELDS.filter(([, re]) => !re.test(body)).map(
|
|
161
|
+
([name]) => name,
|
|
162
|
+
);
|
|
163
|
+
if (!_hasExecutableSubstance(body)) {
|
|
164
|
+
gaps.push('What to do (no command, path or option set)');
|
|
165
|
+
}
|
|
166
|
+
if (gaps.length) {
|
|
167
|
+
decidability.push({
|
|
168
|
+
line: _lineAt(text, sectionStart + cur.start),
|
|
169
|
+
message: `blocker '${cur.id}' is not decidable yet: ${gaps.join(', ')}`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
108
173
|
}
|
|
109
174
|
}
|
|
110
175
|
|
|
@@ -131,7 +196,8 @@ function _scan(rawText: string): Violation[] {
|
|
|
131
196
|
}
|
|
132
197
|
}
|
|
133
198
|
violations.sort((a, b) => a.line - b.line);
|
|
134
|
-
|
|
199
|
+
decidability.sort((a, b) => a.line - b.line);
|
|
200
|
+
return { hard: violations, decidability };
|
|
135
201
|
}
|
|
136
202
|
|
|
137
203
|
/** Sorted `agents/roadmaps/*.md` (non-recursive — active roadmaps only). */
|
|
@@ -206,14 +272,18 @@ function main(): number {
|
|
|
206
272
|
return 0;
|
|
207
273
|
}
|
|
208
274
|
let failed = 0;
|
|
275
|
+
const undecidable: Array<{ rel: string; v: Violation }> = [];
|
|
209
276
|
for (const roadmap of roadmaps) {
|
|
210
277
|
const rel = _relPosix(roadmap, REPO_ROOT);
|
|
211
278
|
const text = fs.readFileSync(roadmap, 'utf-8');
|
|
212
|
-
const
|
|
213
|
-
|
|
279
|
+
const { hard, decidability } = _scanBoth(text);
|
|
280
|
+
for (const v of decidability) {
|
|
281
|
+
undecidable.push({ rel, v });
|
|
282
|
+
}
|
|
283
|
+
if (hard.length) {
|
|
214
284
|
failed += 1;
|
|
215
285
|
process.stderr.write(`❌ ${rel}\n`);
|
|
216
|
-
for (const v of
|
|
286
|
+
for (const v of hard) {
|
|
217
287
|
process.stderr.write(` line ${v.line}: ${v.message}\n`);
|
|
218
288
|
}
|
|
219
289
|
} else if (!QUIET) {
|
|
@@ -228,8 +298,31 @@ function main(): number {
|
|
|
228
298
|
);
|
|
229
299
|
return 1;
|
|
230
300
|
}
|
|
301
|
+
|
|
302
|
+
// The decidability ratchet. A blocker that names no option, no command and
|
|
303
|
+
// no recommendation is a research task handed to the person with the least
|
|
304
|
+
// context — the defect this half of the gate exists to stop growing.
|
|
305
|
+
const verdict = checkRatchet({
|
|
306
|
+
gate: 'lint_roadmap_blockers:decidability',
|
|
307
|
+
actual: undecidable.length,
|
|
308
|
+
repoRoot: REPO_ROOT,
|
|
309
|
+
});
|
|
310
|
+
if (!verdict.ok) {
|
|
311
|
+
process.stderr.write(`❌ ${verdict.message}\n`);
|
|
312
|
+
for (const { rel, v } of undecidable) {
|
|
313
|
+
process.stderr.write(` ${rel}:${v.line}: ${v.message}\n`);
|
|
314
|
+
}
|
|
315
|
+
process.stderr.write(
|
|
316
|
+
'\n Every open blocker needs **Recommendation:** (which option, and why),\n' +
|
|
317
|
+
' **If you do nothing:** (the cost of the non-decision), and a\n' +
|
|
318
|
+
' **What to do:** carrying a command, a path or an enumerated option set.\n' +
|
|
319
|
+
' See templates/roadmaps.md rule 20.\n',
|
|
320
|
+
);
|
|
321
|
+
return 1;
|
|
322
|
+
}
|
|
231
323
|
if (!QUIET) {
|
|
232
324
|
process.stdout.write(`\n✅ ${roadmaps.length} roadmap(s) blocker-contract-clean\n`);
|
|
325
|
+
process.stdout.write(`✅ ${verdict.message}\n`);
|
|
233
326
|
}
|
|
234
327
|
return 0;
|
|
235
328
|
}
|
|
@@ -260,5 +353,15 @@ if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
|
260
353
|
process.exit(main());
|
|
261
354
|
}
|
|
262
355
|
|
|
263
|
-
export {
|
|
264
|
-
|
|
356
|
+
export {
|
|
357
|
+
REPO_ROOT,
|
|
358
|
+
ROADMAP_GLOB,
|
|
359
|
+
REQUIRED_FIELDS,
|
|
360
|
+
DECIDABILITY_FIELDS,
|
|
361
|
+
_hasExecutableSubstance,
|
|
362
|
+
_scan,
|
|
363
|
+
_scanBoth,
|
|
364
|
+
_globRoadmaps,
|
|
365
|
+
main,
|
|
366
|
+
};
|
|
367
|
+
export type { Violation, ScanResult };
|