@event4u/agent-config 8.8.0 → 8.10.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 +139 -263
- package/README.md +5 -5
- package/dist/agent-src/commands/agent-handoff.md +39 -2
- package/dist/agent-src/commands/cost/report.md +13 -3
- package/dist/agent-src/commands/research/deep.md +8 -0
- package/dist/agent-src/commands/research/report.md +7 -0
- package/dist/agent-src/commands/roadmap/create.md +12 -0
- package/dist/agent-src/commands/roadmap/process-full.md +31 -5
- package/dist/agent-src/commands/tests/create.md +28 -6
- package/dist/agent-src/commands/work.md +33 -0
- package/dist/agent-src/commands/worktree/cleanup.md +78 -0
- package/dist/agent-src/commands/worktree/create.md +62 -0
- package/dist/agent-src/commands/worktree/status.md +63 -0
- package/dist/agent-src/commands/worktree/verify.md +70 -0
- package/dist/agent-src/commands/worktree.md +85 -0
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +30 -1
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +60 -31
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +37 -0
- package/dist/agent-src/contexts/execution/subagent-topologies.md +34 -0
- package/dist/agent-src/contexts/execution/verification-mechanics.md +9 -9
- package/dist/agent-src/rules/agent-authority.md +1 -1
- package/dist/agent-src/rules/communication-through-line.md +1 -1
- package/dist/agent-src/rules/delegation-policy.md +5 -0
- package/dist/agent-src/rules/design-fidelity.md +40 -0
- package/dist/agent-src/rules/direct-answers.md +1 -1
- package/dist/agent-src/rules/downstream-changes.md +1 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +65 -0
- package/dist/agent-src/rules/git-history-discipline.md +18 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +6 -1
- package/dist/agent-src/rules/minimal-safe-diff.md +58 -10
- package/dist/agent-src/rules/non-destructive-by-default.md +3 -0
- package/dist/agent-src/rules/question-not-instruction.md +76 -0
- package/dist/agent-src/rules/senior-engineering-discipline.md +1 -1
- package/dist/agent-src/rules/spreadsheet-source-quality.md +61 -0
- package/dist/agent-src/rules/think-before-action.md +1 -0
- package/dist/agent-src/rules/tool-safety.md +8 -0
- package/dist/agent-src/rules/untrusted-input-defense.md +87 -22
- package/dist/agent-src/scripts/update_roadmap_progress.ts +2 -1
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +19 -0
- package/dist/agent-src/skills/accessibility-auditor/evals/evals.json +15 -0
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +10 -3
- package/dist/agent-src/skills/analysis-skill-router/evals/evals.json +13 -0
- package/dist/agent-src/skills/api-testing/SKILL.md +15 -1
- package/dist/agent-src/skills/character-consistency/evals/evals.json +16 -0
- package/dist/agent-src/skills/code-review/SKILL.md +2 -0
- package/dist/agent-src/skills/command-routing/evals/evals.json +14 -0
- package/dist/agent-src/skills/command-writing/SKILL.md +53 -40
- package/dist/agent-src/skills/dashboard-design/evals/evals.json +16 -0
- package/dist/agent-src/skills/dcf-modeling/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/dcf-modeling/evals/evals.json +20 -0
- package/dist/agent-src/skills/decision-record/evals/evals.json +20 -0
- package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +1 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +49 -12
- package/dist/agent-src/skills/design-intelligence/evals/evals.json +14 -0
- package/dist/agent-src/skills/design-review/SKILL.md +35 -15
- package/dist/agent-src/skills/design-system-capture/SKILL.md +84 -24
- package/dist/agent-src/skills/design-system-capture/evals/evals.json +15 -0
- package/dist/agent-src/skills/developer-like-execution/evals/evals.json +16 -0
- package/dist/agent-src/skills/doc-coauthoring/SKILL.md +27 -0
- package/dist/agent-src/skills/doc-coauthoring/evals/evals.json +16 -0
- package/dist/agent-src/skills/editorial-calendar/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/editorial-calendar/evals/evals.json +16 -0
- package/dist/agent-src/skills/estimate-ticket/evals/evals.json +16 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +30 -0
- package/dist/agent-src/skills/existing-ui-audit/evals/triggers.json +47 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +36 -5
- package/dist/agent-src/skills/fe-design/evals/triggers.json +47 -0
- package/dist/agent-src/skills/forecasting/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/forecasting/evals/evals.json +20 -0
- package/dist/agent-src/skills/frontend-render-security/SKILL.md +2 -2
- package/dist/agent-src/skills/fundraising-narrative/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/fundraising-narrative/evals/evals.json +16 -0
- package/dist/agent-src/skills/html-deck/SKILL.md +27 -0
- package/dist/agent-src/skills/iconography/SKILL.md +24 -0
- package/dist/agent-src/skills/incident-commander/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/incident-commander/evals/evals.json +20 -0
- package/dist/agent-src/skills/launch-readiness/evals/evals.json +21 -0
- package/dist/agent-src/skills/logging-monitoring/evals/evals.json +16 -0
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -0
- package/dist/agent-src/skills/mcp/SKILL.md +24 -0
- package/dist/agent-src/skills/mcp-builder/SKILL.md +9 -1
- package/dist/agent-src/skills/memory-consolidation/SKILL.md +55 -4
- package/dist/agent-src/skills/messaging-architecture/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/messaging-architecture/evals/evals.json +16 -0
- package/dist/agent-src/skills/perf-feedback-craft/evals/evals.json +16 -0
- package/dist/agent-src/skills/pest-testing/SKILL.md +1 -1
- package/dist/agent-src/skills/playwright-testing/SKILL.md +16 -0
- package/dist/agent-src/skills/prompt-engineering-patterns/evals/evals.json +16 -0
- package/dist/agent-src/skills/refine-prompt/evals/evals.json +16 -0
- package/dist/agent-src/skills/refine-ticket/evals/evals.json +16 -0
- package/dist/agent-src/skills/release-comms/evals/evals.json +16 -0
- package/dist/agent-src/skills/rice-prioritization/evals/evals.json +20 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +39 -5
- package/dist/agent-src/skills/runway-cognition/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/runway-cognition/evals/evals.json +20 -0
- package/dist/agent-src/skills/scenario-modeling/evals/domain-truth.json +29 -0
- package/dist/agent-src/skills/scenario-modeling/evals/evals.json +20 -0
- package/dist/agent-src/skills/secrets-management/SKILL.md +7 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +12 -3
- package/dist/agent-src/skills/spreadsheet-authoring/SKILL.md +98 -0
- package/dist/agent-src/skills/spreadsheet-authoring/evals/triggers.json +47 -0
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +27 -33
- package/dist/agent-src/skills/supply-chain-intake/SKILL.md +12 -0
- package/dist/agent-src/skills/systematic-debugging/evals/evals.json +20 -0
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +118 -0
- package/dist/agent-src/skills/test-case-discovery/evals/triggers.json +52 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +32 -6
- package/dist/agent-src/skills/test-driven-development/evals/evals.json +16 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +34 -4
- package/dist/agent-src/skills/threat-modeling/evals/evals.json +21 -0
- package/dist/agent-src/skills/typography-system/evals/evals.json +14 -0
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +9 -0
- package/dist/agent-src/skills/unit-economics-modeling/evals/domain-truth.json +40 -0
- package/dist/agent-src/skills/unit-economics-modeling/evals/evals.json +24 -0
- package/dist/agent-src/skills/verify-completion-evidence/evals/evals.json +16 -0
- package/dist/agent-src/skills/vision-articulation/evals/evals.json +16 -0
- package/dist/agent-src/skills/voice-and-tone-design/evals/evals.json +16 -0
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +180 -0
- package/dist/agent-src/skills/worktree-lifecycle/evals/triggers.json +52 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +36 -0
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +326 -67
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +6 -6
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +25 -14
- package/dist/discovery/trust-report.md +4 -4
- package/dist/discovery/workspaces.json +25 -12
- package/dist/install/install.mjs +136 -1
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/claude_builtin_names.js +145 -0
- package/dist/scripts/_lib/claude_builtin_names.js.map +1 -0
- package/dist/scripts/condense.js +14 -0
- package/dist/scripts/condense.js.map +1 -1
- package/docs/CLAIMS.md +36 -8
- package/docs/architecture.md +4 -4
- package/docs/archive/CHANGELOG-pre-8.9.0.md +306 -0
- package/docs/benchmark.md +113 -0
- package/docs/catalog.md +32 -8
- package/docs/command-flows.md +6 -1
- package/docs/contracts/CHANGELOG-conventions.md +6 -0
- package/docs/contracts/command-clusters.md +18 -0
- package/docs/contracts/design-artifact-lifecycle.md +93 -0
- package/docs/contracts/design-artifact-verification.md +153 -0
- package/docs/contracts/injected-block-authenticity-model.md +66 -0
- package/docs/contracts/mcp-tool-inventory.md +28 -28
- package/docs/contracts/release-pr-gating.md +26 -0
- package/docs/contracts/release-sizing.md +85 -0
- package/docs/contracts/surface-agent-contracts.md +153 -0
- package/docs/decisions/ADR-061-corpus-grounding-layer.md +13 -0
- package/docs/decisions/ADR-115-command-cluster-consolidation-phase-4.md +1 -1
- package/docs/decisions/ADR-116-memory-tripwire-activation-path.md +9 -0
- package/docs/distribution/consumer-matrix.md +96 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +27 -0
- package/docs/guidelines/agent-infra/emphasis-budget.md +45 -0
- package/docs/guidelines/agent-infra/simplicity-and-goal-demos.md +151 -0
- package/docs/guidelines/agent-infra/tool-description-as-policy.md +35 -0
- package/docs/guidelines/design-canon.md +1 -0
- package/docs/proof.md +38 -23
- package/docs/release-runbook.md +3 -0
- package/docs/second-brain-scope.md +69 -26
- package/docs/self-review-gate.md +51 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/update_roadmap_progress.ts +2 -1
- package/src/scripts/_dispatch.bash +27 -0
- package/src/scripts/_lib/claude_builtin_names.ts +148 -0
- package/src/scripts/_lib/file_slicer.ts +117 -0
- package/src/scripts/_lib/lexical_index.ts +155 -0
- package/src/scripts/_lib/orchestration_record.ts +183 -0
- package/src/scripts/_lib/orchestration_savings.ts +114 -9
- package/src/scripts/_lib/retrieval_sanitize.ts +71 -0
- package/src/scripts/_lib/security_lint.ts +1 -0
- package/src/scripts/_lib/stat_index.ts +98 -0
- package/src/scripts/benchmark.ts +122 -0
- package/src/scripts/build_proof.ts +30 -17
- package/src/scripts/check_memory.ts +2 -0
- package/src/scripts/check_quality_regression.ts +51 -0
- package/src/scripts/check_release_adjacent_health.ts +121 -0
- package/src/scripts/check_roadmap_trackable.ts +3 -2
- package/src/scripts/check_rule_invariants.ts +275 -0
- package/src/scripts/condense.ts +14 -0
- package/src/scripts/consumer_matrix.ts +353 -0
- package/src/scripts/discovery_graph.ts +0 -0
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/generate_index.ts +6 -2
- package/src/scripts/install-hooks.sh +25 -0
- package/src/scripts/install.ts +35 -2
- package/src/scripts/learning_sidecar.ts +0 -0
- package/src/scripts/lint_agent_skill_names.ts +29 -0
- package/src/scripts/lint_changelog_rollback.ts +293 -0
- package/src/scripts/lint_knowledge_scale.ts +4 -3
- package/src/scripts/lint_roadmap_complexity.ts +124 -0
- package/src/scripts/lint_versioned_cache.ts +195 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +2 -1
- package/src/scripts/mcp_server/tools.ts +24 -1
- package/src/scripts/measure_lexical_ranking.ts +173 -0
- package/src/scripts/memory_lookup.ts +0 -0
- package/src/scripts/orchestration_record.ts +156 -0
- package/src/scripts/orchestration_savings_report.ts +65 -8
- package/src/scripts/schemas/skill.schema.json +4 -0
- package/src/scripts/score_domain_truth.ts +192 -0
- package/src/scripts/second_brain_retrieval.ts +320 -0
- package/src/scripts/second_brain_run.ts +341 -0
- package/src/scripts/self_review_gate.test.ts +65 -0
- package/src/scripts/self_review_gate.ts +272 -0
- package/src/scripts/trigger_eval_grandfather.json +223 -224
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* CHANGELOG `Rollback:` linter.
|
|
4
|
+
*
|
|
5
|
+
* Normative source: `docs/contracts/release-sizing.md § Rollback path`.
|
|
6
|
+
* Every NEW minor / major section in `CHANGELOG.md` — a heading matching
|
|
7
|
+
* the repo's `## [X.Y.0](compare-link) (date)` shape (patch component 0)
|
|
8
|
+
* — must contain at least one `Rollback:` line anywhere in its section,
|
|
9
|
+
* naming how a consumer undoes the subsystem the release introduced or
|
|
10
|
+
* substantially reworked.
|
|
11
|
+
*
|
|
12
|
+
* Cutoff — decidable, no retro-fail: the gate only fires for section
|
|
13
|
+
* versions **strictly greater** than the current `package.json` version
|
|
14
|
+
* at lint time. Historical sections are grandfathered by construction.
|
|
15
|
+
* `--cutoff X.Y.Z` overrides the cutoff (e.g. the release pipeline can
|
|
16
|
+
* pass the PREVIOUS released version so the section it is about to cut
|
|
17
|
+
* is itself in scope).
|
|
18
|
+
*
|
|
19
|
+
* Shares the heading / era regexes with the drift gate via
|
|
20
|
+
* `src/scripts/_lib/changelog_eras.ts` — no parallel copies.
|
|
21
|
+
*
|
|
22
|
+
* CLI: exit 0 clean, 1 violations (or selftest failure), 2 usage error.
|
|
23
|
+
* `--selftest` proves the rule on inline fixtures: a minor section
|
|
24
|
+
* without `Rollback:` fails; one with it passes.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as fs from 'node:fs';
|
|
28
|
+
import * as path from 'node:path';
|
|
29
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
CHANGELOG,
|
|
33
|
+
REPO_ROOT,
|
|
34
|
+
VERSION_HEADING_RE,
|
|
35
|
+
} from './_lib/changelog_eras.js';
|
|
36
|
+
|
|
37
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
38
|
+
|
|
39
|
+
const ROLLBACK_LINE_RE = /\bRollback:/;
|
|
40
|
+
// Section ends at the next version heading, the next era header, or any
|
|
41
|
+
// other level-1/2 heading (`## [Unreleased]`, `# Era: …`). `###` sub-
|
|
42
|
+
// headings stay inside the section.
|
|
43
|
+
const SECTION_BOUNDARY_RE = /^##? /;
|
|
44
|
+
|
|
45
|
+
export interface Violation {
|
|
46
|
+
readonly version: string;
|
|
47
|
+
readonly line: number; // 1-based heading line
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parse_semver(v: string): [number, number, number] | null {
|
|
51
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
|
|
52
|
+
if (!m) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function semver_gt(a: [number, number, number], b: [number, number, number]): boolean {
|
|
59
|
+
if (a[0] !== b[0]) return a[0] > b[0];
|
|
60
|
+
if (a[1] !== b[1]) return a[1] > b[1];
|
|
61
|
+
return a[2] > b[2];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Read the cutoff version from package.json at the repo root. */
|
|
65
|
+
function package_json_version(): string {
|
|
66
|
+
const pkg_path = path.join(REPO_ROOT, 'package.json');
|
|
67
|
+
const pkg = JSON.parse(fs.readFileSync(pkg_path, 'utf-8')) as { version?: string };
|
|
68
|
+
if (typeof pkg.version !== 'string' || parse_semver(pkg.version) === null) {
|
|
69
|
+
throw new Error(`package.json version is not bare semver: '${String(pkg.version)}'`);
|
|
70
|
+
}
|
|
71
|
+
return pkg.version;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Core check: return one Violation per minor/major section (`## [X.Y.0]`)
|
|
76
|
+
* whose version is strictly greater than `cutoff` and whose body carries
|
|
77
|
+
* no `Rollback:` line.
|
|
78
|
+
*/
|
|
79
|
+
export function check_rollback_lines(lines: readonly string[], cutoff: string): Violation[] {
|
|
80
|
+
const cut = parse_semver(cutoff);
|
|
81
|
+
if (cut === null) {
|
|
82
|
+
throw new Error(`cutoff is not bare semver (X.Y.Z): '${cutoff}'`);
|
|
83
|
+
}
|
|
84
|
+
const violations: Violation[] = [];
|
|
85
|
+
for (let i = 0; i < lines.length; i++) {
|
|
86
|
+
const m = VERSION_HEADING_RE.exec(lines[i]!);
|
|
87
|
+
if (!m) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const version = m.groups!['version']!;
|
|
91
|
+
const parsed = parse_semver(version);
|
|
92
|
+
if (parsed === null || parsed[2] !== 0) {
|
|
93
|
+
continue; // patch release — not a minor/major section
|
|
94
|
+
}
|
|
95
|
+
if (!semver_gt(parsed, cut)) {
|
|
96
|
+
continue; // at or below the cutoff — grandfathered
|
|
97
|
+
}
|
|
98
|
+
// Section body: heading line down to (exclusive) the next boundary.
|
|
99
|
+
let end = lines.length;
|
|
100
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
101
|
+
if (SECTION_BOUNDARY_RE.test(lines[j]!)) {
|
|
102
|
+
end = j;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const has_rollback = lines.slice(i, end).some((ln) => ROLLBACK_LINE_RE.test(ln));
|
|
107
|
+
if (!has_rollback) {
|
|
108
|
+
violations.push({ version, line: i + 1 });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return violations;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── selftest fixtures ─────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
function _fixture(section_body: string): string[] {
|
|
117
|
+
return (
|
|
118
|
+
'# Changelog\n\n' +
|
|
119
|
+
'## [Unreleased]\n\n' +
|
|
120
|
+
'# Era: 9.1.x — current\n\n' +
|
|
121
|
+
'> Started at `9.1.0`.\n\n' +
|
|
122
|
+
section_body +
|
|
123
|
+
'# Era: pre-9.0.0 — archived\n'
|
|
124
|
+
).split('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function run_selftest(): number {
|
|
128
|
+
const cutoff = '9.0.0';
|
|
129
|
+
let failed = 0;
|
|
130
|
+
const assert = (label: string, ok: boolean): void => {
|
|
131
|
+
process.stdout.write(`${ok ? '✅' : '❌'} selftest: ${label}\n`);
|
|
132
|
+
if (!ok) failed += 1;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// 1. New minor section WITHOUT Rollback: → exactly one violation.
|
|
136
|
+
const red = check_rollback_lines(
|
|
137
|
+
_fixture(
|
|
138
|
+
'## [9.1.0](https://example/compare/9.0.0...9.1.0) (2026-07-10)\n\n' +
|
|
139
|
+
'### Features\n\n* **workspace:** new subsystem ([abc1234](https://example))\n\n',
|
|
140
|
+
),
|
|
141
|
+
cutoff,
|
|
142
|
+
);
|
|
143
|
+
assert('minor section without Rollback: FAILS', red.length === 1 && red[0]!.version === '9.1.0');
|
|
144
|
+
|
|
145
|
+
// 2. Same section WITH a Rollback: line → clean.
|
|
146
|
+
const green = check_rollback_lines(
|
|
147
|
+
_fixture(
|
|
148
|
+
'## [9.1.0](https://example/compare/9.0.0...9.1.0) (2026-07-10)\n\n' +
|
|
149
|
+
'### Features\n\n* **workspace:** new subsystem ([abc1234](https://example))\n' +
|
|
150
|
+
' Rollback: set `workspace.enabled: false` in `.agent-settings.yml`.\n\n',
|
|
151
|
+
),
|
|
152
|
+
cutoff,
|
|
153
|
+
);
|
|
154
|
+
assert('minor section with Rollback: passes', green.length === 0);
|
|
155
|
+
|
|
156
|
+
// 3. Historical minor section at/below the cutoff → grandfathered.
|
|
157
|
+
const old = check_rollback_lines(
|
|
158
|
+
_fixture('## [9.0.0](https://example/compare/8.9.0...9.0.0) (2026-06-01)\n\n* old entry\n\n'),
|
|
159
|
+
cutoff,
|
|
160
|
+
);
|
|
161
|
+
assert('section at cutoff is grandfathered', old.length === 0);
|
|
162
|
+
|
|
163
|
+
// 4. New PATCH section without Rollback: → out of scope.
|
|
164
|
+
const patch = check_rollback_lines(
|
|
165
|
+
_fixture('## [9.1.1](https://example/compare/9.1.0...9.1.1) (2026-07-11)\n\n* a fix\n\n'),
|
|
166
|
+
cutoff,
|
|
167
|
+
);
|
|
168
|
+
assert('patch section is out of scope', patch.length === 0);
|
|
169
|
+
|
|
170
|
+
// 5. Rollback: in a FOLLOWING section must not satisfy the earlier one.
|
|
171
|
+
const bleed = check_rollback_lines(
|
|
172
|
+
_fixture(
|
|
173
|
+
'## [9.2.0](https://example/compare/9.1.0...9.2.0) (2026-08-01)\n\n* no rollback here\n\n' +
|
|
174
|
+
'## [9.1.0](https://example/compare/9.0.0...9.1.0) (2026-07-10)\n\n' +
|
|
175
|
+
'Rollback: only covers 9.1.0.\n\n',
|
|
176
|
+
),
|
|
177
|
+
cutoff,
|
|
178
|
+
);
|
|
179
|
+
assert('Rollback: does not bleed across sections', bleed.length === 1 && bleed[0]!.version === '9.2.0');
|
|
180
|
+
|
|
181
|
+
if (failed > 0) {
|
|
182
|
+
process.stderr.write(`❌ selftest: ${failed} fixture(s) failed.\n`);
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
process.stdout.write('✅ selftest: all fixtures behave as specified.\n');
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── CLI ───────────────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
interface ParsedArgs {
|
|
192
|
+
selftest: boolean;
|
|
193
|
+
cutoff: string | null;
|
|
194
|
+
changelog: string | null;
|
|
195
|
+
quiet: boolean;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function _argparse_error(message: string): never {
|
|
199
|
+
process.stderr.write(`lint_changelog_rollback: error: ${message}\n`);
|
|
200
|
+
process.exit(2);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parse_args(argv: readonly string[]): ParsedArgs {
|
|
204
|
+
const args: ParsedArgs = { selftest: false, cutoff: null, changelog: null, quiet: false };
|
|
205
|
+
for (let i = 0; i < argv.length; i++) {
|
|
206
|
+
const arg = argv[i]!;
|
|
207
|
+
if (arg === '--selftest') {
|
|
208
|
+
args.selftest = true;
|
|
209
|
+
} else if (arg === '--cutoff') {
|
|
210
|
+
const v = argv[++i];
|
|
211
|
+
if (v === undefined) _argparse_error('argument --cutoff: expected one argument');
|
|
212
|
+
args.cutoff = v;
|
|
213
|
+
} else if (arg.startsWith('--cutoff=')) {
|
|
214
|
+
args.cutoff = arg.slice('--cutoff='.length);
|
|
215
|
+
} else if (arg === '--changelog') {
|
|
216
|
+
const v = argv[++i];
|
|
217
|
+
if (v === undefined) _argparse_error('argument --changelog: expected one argument');
|
|
218
|
+
args.changelog = v;
|
|
219
|
+
} else if (arg.startsWith('--changelog=')) {
|
|
220
|
+
args.changelog = arg.slice('--changelog='.length);
|
|
221
|
+
} else if (arg === '--quiet') {
|
|
222
|
+
args.quiet = true;
|
|
223
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
224
|
+
process.stdout.write(
|
|
225
|
+
'usage: lint_changelog_rollback [-h] [--selftest] [--cutoff X.Y.Z] [--changelog PATH] [--quiet]\n',
|
|
226
|
+
);
|
|
227
|
+
process.exit(0);
|
|
228
|
+
} else {
|
|
229
|
+
_argparse_error(`unrecognized arguments: ${arg}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return args;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function main(argv?: readonly string[]): number {
|
|
236
|
+
const args = parse_args(argv ?? process.argv.slice(2));
|
|
237
|
+
if (args.selftest) {
|
|
238
|
+
return run_selftest();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const changelog_path = args.changelog ?? CHANGELOG;
|
|
242
|
+
const cutoff = args.cutoff ?? package_json_version();
|
|
243
|
+
const text = fs.readFileSync(changelog_path, 'utf-8');
|
|
244
|
+
const lines = text.split(/\r\n|\r|\n/);
|
|
245
|
+
|
|
246
|
+
const violations = check_rollback_lines(lines, cutoff);
|
|
247
|
+
if (violations.length > 0) {
|
|
248
|
+
process.stderr.write(
|
|
249
|
+
`❌ ${violations.length} minor/major CHANGELOG section(s) above ` +
|
|
250
|
+
`cutoff ${cutoff} without a Rollback: line:\n`,
|
|
251
|
+
);
|
|
252
|
+
for (const v of violations) {
|
|
253
|
+
process.stderr.write(` - ## [${v.version}] (line ${v.line})\n`);
|
|
254
|
+
}
|
|
255
|
+
process.stderr.write(
|
|
256
|
+
'\nEvery new minor/major section must name a rollback path for each ' +
|
|
257
|
+
'introduced or substantially reworked subsystem via a `Rollback:` ' +
|
|
258
|
+
'line — see docs/contracts/release-sizing.md § Rollback path.\n',
|
|
259
|
+
);
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!args.quiet) {
|
|
264
|
+
process.stdout.write(
|
|
265
|
+
`✅ CHANGELOG Rollback: gate clean (cutoff ${cutoff}, ` +
|
|
266
|
+
`${path.relative(REPO_ROOT, changelog_path)}).\n`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function _isCliEntry(): boolean {
|
|
273
|
+
if (process.argv[1] === undefined) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
277
|
+
if (import.meta.url === argvUrl) {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
282
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
283
|
+
return here === argv;
|
|
284
|
+
} catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
290
|
+
process.exit(main());
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export { main, run_selftest, parse_semver, semver_gt, package_json_version };
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
* - sessions scale — `agents/knowledge/sessions/` > 50 pages
|
|
15
15
|
* → design fold-through-consolidate-gate for tracked pages
|
|
16
16
|
* - type scale — any single memory/knowledge type > 200 files
|
|
17
|
-
* →
|
|
17
|
+
* → rank via `_lib/lexical_index.ts` (hand-rolled BM25 +
|
|
18
|
+
* trigram, no engine fork / no FTS5; ADR-061 honoured)
|
|
18
19
|
* - corpus scale — > 500 files across all types
|
|
19
|
-
* → same
|
|
20
|
+
* → same lexical-index activation path
|
|
20
21
|
* - hot-context size — `agents/runtime/state/hot-context.md` > 600 tokens
|
|
21
22
|
* (estimated at 4 chars/token) → trim schema / fix the
|
|
22
23
|
* deterministic writer
|
|
@@ -151,7 +152,7 @@ export function runChecks(root: string): Warning[] {
|
|
|
151
152
|
metric: `${n}/${TYPE_FILES_MAX}`,
|
|
152
153
|
message:
|
|
153
154
|
`type '${type}' holds ${n} files (> ${TYPE_FILES_MAX}). ` +
|
|
154
|
-
'Activation path (
|
|
155
|
+
'Activation path (resolved, road-to-retrieval-substrate-hardening B2): rank via the hand-rolled BM25 + trigram index in `_lib/lexical_index.ts` (pure stdlib, NO engine fork / NO SQLite-FTS5, ADR-061 honoured). Measured lift: mean tie-set 3.333 → 1.0 (internal/bench/reports/lexical-ranking.json). Wire it lazily at first lookup + a stat-index, no vectors, no service.',
|
|
155
156
|
});
|
|
156
157
|
}
|
|
157
158
|
}
|
|
@@ -190,6 +190,9 @@ function lint_roadmap(
|
|
|
190
190
|
_check_execution_mode(fm, problems);
|
|
191
191
|
if (warnings) {
|
|
192
192
|
_check_autonomous_authoring(text, fm, warnings);
|
|
193
|
+
_check_human_gate_steps(text, warnings);
|
|
194
|
+
_check_human_gate_phase_headings(text, warnings);
|
|
195
|
+
_check_human_gate_exit_criteria(text, warnings);
|
|
193
196
|
}
|
|
194
197
|
return problems;
|
|
195
198
|
}
|
|
@@ -236,6 +239,127 @@ function _check_autonomous_authoring(text: string, fm: string, warnings: string[
|
|
|
236
239
|
}
|
|
237
240
|
}
|
|
238
241
|
|
|
242
|
+
// Human-gate step patterns (templates/roadmaps.md rule 22) — checkbox
|
|
243
|
+
// steps that park work on a human instead of the agent. Steps are
|
|
244
|
+
// agent-executable by default; genuinely user-clearable gates belong in
|
|
245
|
+
// a structured `## Blockers` entry, agent-checkable verifications become
|
|
246
|
+
// commands / targeted tests. Warnings never fail the lint; they fire for
|
|
247
|
+
// every execution mode (an interactive roadmap with sprinkled human-check
|
|
248
|
+
// steps is the same authoring bug, just slower to hurt).
|
|
249
|
+
const HUMAN_GATE_STEP_PATS: Array<[RegExp, string]> = [
|
|
250
|
+
[
|
|
251
|
+
/\b(user|maintainer|human|operator|stakeholder|product owner) (verifies|reviews|approves|confirms|signs?[- ]off(?: on)?|inspects|validates)\b/i,
|
|
252
|
+
"'<human> verifies / reviews / approves …' step",
|
|
253
|
+
],
|
|
254
|
+
[
|
|
255
|
+
/\bmanually (verify|review|check|test|confirm|inspect|validate)\b/i,
|
|
256
|
+
"'manually verify / check …' step",
|
|
257
|
+
],
|
|
258
|
+
[
|
|
259
|
+
/\bwait for (the )?(user|maintainer|stakeholders?|approval|confirmation|review|feedback)\b/i,
|
|
260
|
+
"'wait for approval / user' step",
|
|
261
|
+
],
|
|
262
|
+
[/\bask the (user|maintainer)\b/i, "'ask the user' step"],
|
|
263
|
+
[
|
|
264
|
+
/\b(get|obtain|request|await|collect) ((the|an?|final) )?((user|maintainer|stakeholder|product owner|team) )?(approval|confirmation|sign[- ]?off)\b/i,
|
|
265
|
+
"'obtain approval / sign-off' step",
|
|
266
|
+
],
|
|
267
|
+
[/\bsign[- ]?off (from|by)\b/i, "'sign-off from <human>' step"],
|
|
268
|
+
[/\bhuman review (required|needed|gate)\b/i, "'human review required' step"],
|
|
269
|
+
[
|
|
270
|
+
/\b(confirm|check|verify|review|clarify) with the (user|maintainer)\b/i,
|
|
271
|
+
"'confirm with the user' step",
|
|
272
|
+
],
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
// Human-gate phase headings (template rule 22) — a phase whose TITLE is
|
|
276
|
+
// the human checkpoint ("## Phase 5 — Review / Sign-off", "## User
|
|
277
|
+
// Acceptance"). Whole-heading match after an optional "Phase N" prefix,
|
|
278
|
+
// so working headings ("## Review existing skills") never fire.
|
|
279
|
+
const HUMAN_GATE_HEADING_PAT =
|
|
280
|
+
/^#{2,3}\s+(?:phase\s+\d+\s*(?:[—–:.-])?\s*)?(?:review(?:\s*(?:[/&+]|and)\s*sign[- ]?off)?|sign[- ]?off|approval|user\s+acceptance(?:\s+testing)?|manual\s+(?:verification|review|testing|qa)|human\s+(?:validation|review))\s*$/i;
|
|
281
|
+
|
|
282
|
+
// Human-approval exit / acceptance criteria (template rule 22) — criteria
|
|
283
|
+
// must be agent-decidable (exit code, file exists, test passes), never
|
|
284
|
+
// "user approves" / "looks good". Applied inside Exit-criteria /
|
|
285
|
+
// Acceptance blocks only.
|
|
286
|
+
const HUMAN_GATE_CRITERIA_PAT =
|
|
287
|
+
/\b(user|maintainer|human|operator|stakeholder|product owner)\b[^\n]{0,60}\b(approv\w*|confirm\w*|sign[- ]?off|reviews)\b|\b(approval|confirmation|sign[- ]?off)\s+(from|by|received)\b|\blooks good\b/i;
|
|
288
|
+
|
|
289
|
+
const EXIT_CRITERIA_OPENER_PAT = /^\s*(?:[-*]\s+)?\*{0,2}(exit criteria|acceptance(?: criteria)?)\*{0,2}\s*:?/i;
|
|
290
|
+
|
|
291
|
+
const RULE22_HINT =
|
|
292
|
+
'templates/roadmaps.md rule 22: steps and criteria are agent-decidable; ' +
|
|
293
|
+
'replace with an agent-verifiable check (command / targeted test) or, ' +
|
|
294
|
+
'if only a human can decide/authorize, a structured ## Blockers entry';
|
|
295
|
+
|
|
296
|
+
function _check_human_gate_steps(text: string, warnings: string[]): void {
|
|
297
|
+
for (const rawLine of text.split('\n')) {
|
|
298
|
+
if (!/^\s*-\s*\[ \]/.test(rawLine)) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
for (const [re, label] of HUMAN_GATE_STEP_PATS) {
|
|
302
|
+
if (re.test(rawLine)) {
|
|
303
|
+
warnings.push(
|
|
304
|
+
`human-gate step (${label}): ` +
|
|
305
|
+
`"${rawLine.trim().slice(0, 80)}" — ${RULE22_HINT}`,
|
|
306
|
+
);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function _check_human_gate_phase_headings(text: string, warnings: string[]): void {
|
|
314
|
+
for (const rawLine of text.split('\n')) {
|
|
315
|
+
if (!rawLine.startsWith('#')) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (HUMAN_GATE_HEADING_PAT.test(rawLine)) {
|
|
319
|
+
warnings.push(
|
|
320
|
+
`human-gate phase heading: "${rawLine.trim().slice(0, 80)}" — ` +
|
|
321
|
+
'a phase whose work only a human can finish blocks autonomous ' +
|
|
322
|
+
`execution; ${RULE22_HINT}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function _check_human_gate_exit_criteria(text: string, warnings: string[]): void {
|
|
329
|
+
let inHeadingBlock = false;
|
|
330
|
+
let inInlineBlock = false;
|
|
331
|
+
for (const rawLine of text.split('\n')) {
|
|
332
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(rawLine);
|
|
333
|
+
if (heading) {
|
|
334
|
+
inHeadingBlock = /\b(acceptance|exit criteria)\b/i.test(heading[1]!);
|
|
335
|
+
inInlineBlock = false;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (rawLine.trim() === '') {
|
|
339
|
+
inInlineBlock = false;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const opener = EXIT_CRITERIA_OPENER_PAT.test(rawLine);
|
|
343
|
+
if (opener) {
|
|
344
|
+
inInlineBlock = true;
|
|
345
|
+
}
|
|
346
|
+
if (!inHeadingBlock && !inInlineBlock) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
// Open-checkbox lines are covered by _check_human_gate_steps;
|
|
350
|
+
// done/deferred/cancelled lines record history, not future gates.
|
|
351
|
+
if (/^\s*-\s*\[[ xX~-]\]/.test(rawLine)) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (HUMAN_GATE_CRITERIA_PAT.test(rawLine)) {
|
|
355
|
+
warnings.push(
|
|
356
|
+
`human-approval exit criterion: "${rawLine.trim().slice(0, 80)}" — ` +
|
|
357
|
+
`criteria must be agent-decidable; ${RULE22_HINT}`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
239
363
|
function _check_execution_mode(fm: string, problems: string[]): void {
|
|
240
364
|
if (!fm || !/^execution:/m.test(fm)) {
|
|
241
365
|
return; // absent field = interactive default — valid
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Versioned-cache lint (road-to-retrieval-substrate-hardening B5b).
|
|
4
|
+
*
|
|
5
|
+
* Every derived cache the suite writes MUST be safe to invalidate when its
|
|
6
|
+
* producer changes — otherwise a tool-version bump silently reads a
|
|
7
|
+
* stale-format cache and corrupts the read path. This lint makes that
|
|
8
|
+
* discipline structural: any cache-file path literal in `src/scripts/**`
|
|
9
|
+
* must EITHER carry a version namespace in the path (`v<N>`, a `${…version…}`
|
|
10
|
+
* interpolation, or a `_VERSION`/`schema_version` reference), OR carry an
|
|
11
|
+
* explicit invalidation justification comment (`cache-version:` /
|
|
12
|
+
* `cache-invalidation:`) within a few lines — an author's ack that the cache
|
|
13
|
+
* is invalidated by another mechanism (full overwrite, content hash, …).
|
|
14
|
+
*
|
|
15
|
+
* It is a FAILING gate (this roadmap introduces it), scoped tightly to
|
|
16
|
+
* cache-file SUFFIXES so it has near-zero false-positive surface: it fires on
|
|
17
|
+
* the derived caches later phases create (B2 index, B3 learning sidecar, B5a
|
|
18
|
+
* stat-index), not on directories or ordinary data files.
|
|
19
|
+
*
|
|
20
|
+
* Usage: lint_versioned_cache.ts [--dir <root>] [--format text|json] [--quiet]
|
|
21
|
+
* Exit codes: 0 = clean, 1 = usage error, 2 = violations found, 3 = internal.
|
|
22
|
+
*/
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as path from 'node:path';
|
|
25
|
+
import process from 'node:process';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const PROG = 'lint_versioned_cache.ts';
|
|
29
|
+
|
|
30
|
+
/** Window (lines) around a cache-path literal scanned for a justification. */
|
|
31
|
+
export const JUSTIFY_WINDOW = 6;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A path literal is a "derived cache file" when its basename matches one of
|
|
35
|
+
* these suffixes. Suffix-based (not dir-based) to keep the trigger precise —
|
|
36
|
+
* ordinary data/config JSON does not match.
|
|
37
|
+
*/
|
|
38
|
+
export const CACHE_SUFFIX_RE =
|
|
39
|
+
/(?:^|[\/'"`\-.])(?:[a-z0-9_\-.]*)(?:-index|\.index|-cache|\.cache|\.stat-index|-stat-index|\.agent-learning)\.json(?:['"`]|$)/i;
|
|
40
|
+
|
|
41
|
+
/** A version namespace present in the path literal itself. */
|
|
42
|
+
const VERSION_IN_PATH_RE = /(?:[\/_\-.]v\d+|\$\{[^}]*[Vv]ersion[^}]*\}|\$\{[^}]*VERSION[^}]*\})/;
|
|
43
|
+
|
|
44
|
+
/** An explicit author justification comment near the literal. */
|
|
45
|
+
const JUSTIFY_RE = /(?:cache-version:|cache-invalidation:)/i;
|
|
46
|
+
|
|
47
|
+
/** A string / template literal containing a cache-file path. */
|
|
48
|
+
const LITERAL_RE = /(['"`])((?:\\.|(?!\1).)*?)\1/g;
|
|
49
|
+
|
|
50
|
+
export interface Violation {
|
|
51
|
+
file: string;
|
|
52
|
+
line: number; // 1-indexed
|
|
53
|
+
literal: string;
|
|
54
|
+
message: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function listTsFiles(root: string): string[] {
|
|
58
|
+
const out: string[] = [];
|
|
59
|
+
const skip = new Set([
|
|
60
|
+
'node_modules',
|
|
61
|
+
'.git',
|
|
62
|
+
'dist',
|
|
63
|
+
'coverage',
|
|
64
|
+
'.turbo',
|
|
65
|
+
'.cache',
|
|
66
|
+
'__pycache__',
|
|
67
|
+
]);
|
|
68
|
+
const walk = (dir: string): void => {
|
|
69
|
+
let entries: fs.Dirent[];
|
|
70
|
+
try {
|
|
71
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
72
|
+
} catch {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
for (const e of entries) {
|
|
76
|
+
if (e.isDirectory()) {
|
|
77
|
+
if (!skip.has(e.name)) walk(path.join(dir, e.name));
|
|
78
|
+
} else if (e.isFile() && e.name.endsWith('.ts') && !e.name.endsWith('.d.ts')) {
|
|
79
|
+
out.push(path.join(dir, e.name));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
walk(root);
|
|
84
|
+
return out.sort();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _isCachePathLiteral(value: string): boolean {
|
|
88
|
+
// Reset lastIndex — CACHE_SUFFIX_RE has no /g flag, so test() is safe.
|
|
89
|
+
return CACHE_SUFFIX_RE.test(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function scanFile(file: string, source: string): Violation[] {
|
|
93
|
+
const lines = source.split('\n');
|
|
94
|
+
const violations: Violation[] = [];
|
|
95
|
+
for (let i = 0; i < lines.length; i++) {
|
|
96
|
+
const text = lines[i] as string;
|
|
97
|
+
LITERAL_RE.lastIndex = 0;
|
|
98
|
+
let m: RegExpExecArray | null;
|
|
99
|
+
while ((m = LITERAL_RE.exec(text)) !== null) {
|
|
100
|
+
const value = m[2] as string;
|
|
101
|
+
if (!_isCachePathLiteral(value)) continue;
|
|
102
|
+
if (VERSION_IN_PATH_RE.test(value)) continue; // versioned in the path itself
|
|
103
|
+
|
|
104
|
+
// Look for a justification comment in the window around the literal.
|
|
105
|
+
const from = Math.max(0, i - JUSTIFY_WINDOW);
|
|
106
|
+
const to = Math.min(lines.length - 1, i + JUSTIFY_WINDOW);
|
|
107
|
+
let justified = false;
|
|
108
|
+
for (let j = from; j <= to; j++) {
|
|
109
|
+
if (JUSTIFY_RE.test(lines[j] as string)) {
|
|
110
|
+
justified = true;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (justified) continue;
|
|
115
|
+
|
|
116
|
+
violations.push({
|
|
117
|
+
file,
|
|
118
|
+
line: i + 1,
|
|
119
|
+
literal: value,
|
|
120
|
+
message:
|
|
121
|
+
'derived cache without a version namespace — add a `v<N>` (or ' +
|
|
122
|
+
'`${…version…}`) segment to the path, or an inline ' +
|
|
123
|
+
'`// cache-invalidation: <why no version is needed>` comment',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return violations;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function runChecks(root: string): Violation[] {
|
|
131
|
+
const violations: Violation[] = [];
|
|
132
|
+
for (const file of listTsFiles(root)) {
|
|
133
|
+
// Never lint this file itself — its regexes contain the very suffixes
|
|
134
|
+
// it hunts for.
|
|
135
|
+
if (path.basename(file) === 'lint_versioned_cache.ts') continue;
|
|
136
|
+
let source: string;
|
|
137
|
+
try {
|
|
138
|
+
source = fs.readFileSync(file, 'utf-8');
|
|
139
|
+
} catch {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
violations.push(...scanFile(file, source));
|
|
143
|
+
}
|
|
144
|
+
return violations;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function main(argv: string[]): number {
|
|
148
|
+
let dir = 'src/scripts';
|
|
149
|
+
let format: 'text' | 'json' = 'text';
|
|
150
|
+
let quiet = false;
|
|
151
|
+
for (let i = 0; i < argv.length; i++) {
|
|
152
|
+
const a = argv[i] as string;
|
|
153
|
+
if (a === '--dir') {
|
|
154
|
+
dir = argv[++i] ?? dir;
|
|
155
|
+
} else if (a.startsWith('--dir=')) {
|
|
156
|
+
dir = a.slice('--dir='.length);
|
|
157
|
+
} else if (a === '--format') {
|
|
158
|
+
format = (argv[++i] as 'text' | 'json') ?? 'text';
|
|
159
|
+
} else if (a === '--quiet') {
|
|
160
|
+
quiet = true;
|
|
161
|
+
} else if (a === '-h' || a === '--help') {
|
|
162
|
+
process.stdout.write(`usage: ${PROG} [--dir <root>] [--format text|json] [--quiet]\n`);
|
|
163
|
+
return 0;
|
|
164
|
+
} else {
|
|
165
|
+
process.stderr.write(`${PROG}: error: unknown argument ${a}\n`);
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let violations: Violation[];
|
|
171
|
+
try {
|
|
172
|
+
violations = runChecks(path.resolve(dir));
|
|
173
|
+
} catch (exc) {
|
|
174
|
+
process.stderr.write(`${PROG}: internal error: ${String(exc)}\n`);
|
|
175
|
+
return 3;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (format === 'json') {
|
|
179
|
+
process.stdout.write(JSON.stringify({ violations }, null, 2) + '\n');
|
|
180
|
+
return violations.length > 0 ? 2 : 0;
|
|
181
|
+
}
|
|
182
|
+
if (violations.length === 0) {
|
|
183
|
+
if (!quiet) process.stdout.write(`${PROG}: no unversioned derived caches\n`);
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
for (const v of violations) {
|
|
187
|
+
process.stdout.write(`❌ ${v.file}:${v.line} — cache '${v.literal}': ${v.message}\n`);
|
|
188
|
+
}
|
|
189
|
+
process.stdout.write(`${PROG}: ${violations.length} unversioned derived cache(s)\n`);
|
|
190
|
+
return 2;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1] ?? '')) {
|
|
194
|
+
process.exit(main(process.argv.slice(2)));
|
|
195
|
+
}
|
|
@@ -70,7 +70,8 @@
|
|
|
70
70
|
"types": {"type": "array", "items": {"type": "string"}, "minItems": 1, "description": "Memory types to scan, e.g. `historical-patterns`, `incident-learnings`, `ownership`. At least one required."},
|
|
71
71
|
"keys": {"type": "array", "items": {"type": "string"}, "description": "Optional anchor paths or globs to match entries against (e.g. a file you are about to edit)."},
|
|
72
72
|
"limit": {"type": "integer", "minimum": 1, "default": 5, "description": "Maximum entries to return per type. Defaults to 5."},
|
|
73
|
-
"detail": {"type": "string", "enum": ["index", "full"], "default": "full", "description": "'index' returns compact priced rows (id, title, tokens_estimate) instead of full bodies — call this first, then memory_get the ids you need. 'full' (default) returns complete entries."}
|
|
73
|
+
"detail": {"type": "string", "enum": ["index", "full"], "default": "full", "description": "'index' returns compact priced rows (id, title, tokens_estimate) instead of full bodies — call this first, then memory_get the ids you need. 'full' (default) returns complete entries."},
|
|
74
|
+
"token_budget": {"type": "integer", "minimum": 1, "description": "Optional token budget. When set, entries are rendered as one-line compact rows (id, type, confidence, `line`) and the row set is hard-cut at token_budget × 4 chars; omitted hits appear as a top-level `truncation` hint naming a concrete next step. Absent → the envelope is unchanged."}
|
|
74
75
|
},
|
|
75
76
|
"required": ["types"],
|
|
76
77
|
"additionalProperties": false
|