@nforma.ai/nforma 0.2.1
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/LICENSE +22 -0
- package/README.md +1024 -0
- package/agents/qgsd-codebase-mapper.md +764 -0
- package/agents/qgsd-debugger.md +1201 -0
- package/agents/qgsd-executor.md +472 -0
- package/agents/qgsd-integration-checker.md +443 -0
- package/agents/qgsd-phase-researcher.md +502 -0
- package/agents/qgsd-plan-checker.md +643 -0
- package/agents/qgsd-planner.md +1182 -0
- package/agents/qgsd-project-researcher.md +621 -0
- package/agents/qgsd-quorum-orchestrator.md +628 -0
- package/agents/qgsd-quorum-slot-worker.md +41 -0
- package/agents/qgsd-quorum-synthesizer.md +133 -0
- package/agents/qgsd-quorum-test-worker.md +37 -0
- package/agents/qgsd-quorum-worker.md +161 -0
- package/agents/qgsd-research-synthesizer.md +239 -0
- package/agents/qgsd-roadmapper.md +660 -0
- package/agents/qgsd-verifier.md +628 -0
- package/bin/accept-debug-invariant.cjs +165 -0
- package/bin/account-manager.cjs +719 -0
- package/bin/aggregate-requirements.cjs +466 -0
- package/bin/analyze-assumptions.cjs +757 -0
- package/bin/analyze-state-space.cjs +921 -0
- package/bin/attribute-trace-divergence.cjs +150 -0
- package/bin/auth-drivers/gh-cli.cjs +93 -0
- package/bin/auth-drivers/index.cjs +46 -0
- package/bin/auth-drivers/pool.cjs +67 -0
- package/bin/auth-drivers/simple.cjs +95 -0
- package/bin/autoClosePtoF.cjs +110 -0
- package/bin/blessed-terminal.cjs +350 -0
- package/bin/build-phase-index.cjs +472 -0
- package/bin/call-quorum-slot.cjs +541 -0
- package/bin/ccr-secure-config.cjs +99 -0
- package/bin/ccr-secure-start.cjs +83 -0
- package/bin/check-bundled-sdks.cjs +177 -0
- package/bin/check-coverage-guard.cjs +112 -0
- package/bin/check-liveness-fairness.cjs +95 -0
- package/bin/check-mcp-health.cjs +123 -0
- package/bin/check-provider-health.cjs +395 -0
- package/bin/check-results-exit.cjs +24 -0
- package/bin/check-spec-sync.cjs +360 -0
- package/bin/check-trace-redaction.cjs +271 -0
- package/bin/check-trace-schema-drift.cjs +99 -0
- package/bin/compareDrift.cjs +21 -0
- package/bin/conformance-schema.cjs +12 -0
- package/bin/count-scenarios.cjs +420 -0
- package/bin/debt-dedup.cjs +144 -0
- package/bin/debt-ledger.cjs +61 -0
- package/bin/debt-retention.cjs +76 -0
- package/bin/debt-state-machine.cjs +80 -0
- package/bin/detect-coverage-gaps.cjs +204 -0
- package/bin/detect-project-intent.cjs +362 -0
- package/bin/export-prism-constants.cjs +164 -0
- package/bin/extract-annotations.cjs +633 -0
- package/bin/extractFormalExpected.cjs +104 -0
- package/bin/fingerprint-drift.cjs +24 -0
- package/bin/fingerprint-issue.cjs +46 -0
- package/bin/formal-core.cjs +519 -0
- package/bin/formal-ref-linker.cjs +141 -0
- package/bin/formal-test-sync.cjs +788 -0
- package/bin/generate-formal-specs.cjs +588 -0
- package/bin/generate-petri-net.cjs +397 -0
- package/bin/generate-phase-spec.cjs +249 -0
- package/bin/generate-proposed-changes.cjs +194 -0
- package/bin/generate-tla-cfg.cjs +122 -0
- package/bin/generate-traceability-matrix.cjs +701 -0
- package/bin/generate-triage-bundle.cjs +300 -0
- package/bin/gh-account-rotate.cjs +34 -0
- package/bin/initialize-model-registry.cjs +105 -0
- package/bin/install-formal-tools.cjs +382 -0
- package/bin/install.js +2424 -0
- package/bin/isNumericThreshold.cjs +34 -0
- package/bin/issue-classifier.cjs +151 -0
- package/bin/levenshtein.cjs +74 -0
- package/bin/lint-formal-models.cjs +580 -0
- package/bin/load-baseline-requirements.cjs +275 -0
- package/bin/manage-agents-core.cjs +815 -0
- package/bin/migrate-formal-dir.cjs +172 -0
- package/bin/migrate-planning.cjs +206 -0
- package/bin/migrate-to-slots.cjs +255 -0
- package/bin/nForma.cjs +2726 -0
- package/bin/observe-config.cjs +353 -0
- package/bin/observe-debt-writer.cjs +140 -0
- package/bin/observe-handler-grafana.cjs +128 -0
- package/bin/observe-handler-internal.cjs +301 -0
- package/bin/observe-handler-logstash.cjs +153 -0
- package/bin/observe-handler-prometheus.cjs +185 -0
- package/bin/observe-handlers.cjs +436 -0
- package/bin/observe-registry.cjs +131 -0
- package/bin/observe-render.cjs +168 -0
- package/bin/planning-paths.cjs +167 -0
- package/bin/polyrepo.cjs +560 -0
- package/bin/prism-priority.cjs +153 -0
- package/bin/probe-quorum-slots.cjs +167 -0
- package/bin/promote-model.cjs +225 -0
- package/bin/propose-debug-invariants.cjs +165 -0
- package/bin/providers.json +392 -0
- package/bin/pty-proxy.py +129 -0
- package/bin/qgsd-solve.cjs +2477 -0
- package/bin/quorum-consensus-gate.cjs +238 -0
- package/bin/quorum-formal-context.cjs +183 -0
- package/bin/quorum-slot-dispatch.cjs +934 -0
- package/bin/read-policy.cjs +60 -0
- package/bin/requirement-map.cjs +63 -0
- package/bin/requirements-core.cjs +247 -0
- package/bin/resolve-cli.cjs +101 -0
- package/bin/review-mcp-logs.cjs +294 -0
- package/bin/run-account-manager-tlc.cjs +188 -0
- package/bin/run-account-pool-alloy.cjs +158 -0
- package/bin/run-alloy.cjs +153 -0
- package/bin/run-audit-alloy.cjs +187 -0
- package/bin/run-breaker-tlc.cjs +181 -0
- package/bin/run-formal-check.cjs +395 -0
- package/bin/run-formal-verify.cjs +701 -0
- package/bin/run-installer-alloy.cjs +188 -0
- package/bin/run-oauth-rotation-prism.cjs +132 -0
- package/bin/run-oscillation-tlc.cjs +202 -0
- package/bin/run-phase-tlc.cjs +228 -0
- package/bin/run-prism.cjs +446 -0
- package/bin/run-protocol-tlc.cjs +201 -0
- package/bin/run-quorum-composition-alloy.cjs +155 -0
- package/bin/run-sensitivity-sweep.cjs +231 -0
- package/bin/run-stop-hook-tlc.cjs +188 -0
- package/bin/run-tlc.cjs +467 -0
- package/bin/run-transcript-alloy.cjs +173 -0
- package/bin/run-uppaal.cjs +264 -0
- package/bin/secrets.cjs +134 -0
- package/bin/sensitivity-report.cjs +219 -0
- package/bin/sensitivity-sweep-feedback.cjs +194 -0
- package/bin/set-secret.cjs +29 -0
- package/bin/setup-telemetry-cron.sh +36 -0
- package/bin/sweepPtoF.cjs +63 -0
- package/bin/sync-baseline-requirements.cjs +290 -0
- package/bin/task-envelope.cjs +360 -0
- package/bin/telemetry-collector.cjs +229 -0
- package/bin/unified-mcp-server.mjs +735 -0
- package/bin/update-agents.cjs +369 -0
- package/bin/update-scoreboard.cjs +1134 -0
- package/bin/validate-debt-entry.cjs +207 -0
- package/bin/validate-invariant.cjs +419 -0
- package/bin/validate-memory.cjs +389 -0
- package/bin/validate-requirements-haiku.cjs +435 -0
- package/bin/validate-traces.cjs +438 -0
- package/bin/verify-formal-results.cjs +124 -0
- package/bin/verify-quorum-health.cjs +273 -0
- package/bin/write-check-result.cjs +106 -0
- package/bin/xstate-to-tla.cjs +483 -0
- package/bin/xstate-trace-walker.cjs +205 -0
- package/commands/qgsd/add-phase.md +43 -0
- package/commands/qgsd/add-requirement.md +24 -0
- package/commands/qgsd/add-todo.md +47 -0
- package/commands/qgsd/audit-milestone.md +37 -0
- package/commands/qgsd/check-todos.md +45 -0
- package/commands/qgsd/cleanup.md +18 -0
- package/commands/qgsd/close-formal-gaps.md +33 -0
- package/commands/qgsd/complete-milestone.md +136 -0
- package/commands/qgsd/debug.md +166 -0
- package/commands/qgsd/discuss-phase.md +83 -0
- package/commands/qgsd/execute-phase.md +117 -0
- package/commands/qgsd/fix-tests.md +27 -0
- package/commands/qgsd/formal-test-sync.md +32 -0
- package/commands/qgsd/health.md +22 -0
- package/commands/qgsd/help.md +22 -0
- package/commands/qgsd/insert-phase.md +32 -0
- package/commands/qgsd/join-discord.md +18 -0
- package/commands/qgsd/list-phase-assumptions.md +46 -0
- package/commands/qgsd/map-codebase.md +71 -0
- package/commands/qgsd/map-requirements.md +20 -0
- package/commands/qgsd/mcp-restart.md +176 -0
- package/commands/qgsd/mcp-set-model.md +134 -0
- package/commands/qgsd/mcp-setup.md +1371 -0
- package/commands/qgsd/mcp-status.md +274 -0
- package/commands/qgsd/mcp-update.md +238 -0
- package/commands/qgsd/new-milestone.md +44 -0
- package/commands/qgsd/new-project.md +42 -0
- package/commands/qgsd/observe.md +260 -0
- package/commands/qgsd/pause-work.md +38 -0
- package/commands/qgsd/plan-milestone-gaps.md +34 -0
- package/commands/qgsd/plan-phase.md +44 -0
- package/commands/qgsd/polyrepo.md +50 -0
- package/commands/qgsd/progress.md +24 -0
- package/commands/qgsd/queue.md +54 -0
- package/commands/qgsd/quick.md +133 -0
- package/commands/qgsd/quorum-test.md +275 -0
- package/commands/qgsd/quorum.md +707 -0
- package/commands/qgsd/reapply-patches.md +110 -0
- package/commands/qgsd/remove-phase.md +31 -0
- package/commands/qgsd/research-phase.md +189 -0
- package/commands/qgsd/resume-work.md +40 -0
- package/commands/qgsd/set-profile.md +34 -0
- package/commands/qgsd/settings.md +39 -0
- package/commands/qgsd/solve.md +565 -0
- package/commands/qgsd/sync-baselines.md +119 -0
- package/commands/qgsd/triage.md +233 -0
- package/commands/qgsd/update.md +37 -0
- package/commands/qgsd/verify-work.md +38 -0
- package/hooks/dist/config-loader.js +297 -0
- package/hooks/dist/conformance-schema.cjs +12 -0
- package/hooks/dist/gsd-context-monitor.js +64 -0
- package/hooks/dist/qgsd-check-update.js +62 -0
- package/hooks/dist/qgsd-circuit-breaker.js +682 -0
- package/hooks/dist/qgsd-precompact.js +156 -0
- package/hooks/dist/qgsd-prompt.js +653 -0
- package/hooks/dist/qgsd-session-start.js +122 -0
- package/hooks/dist/qgsd-slot-correlator.js +58 -0
- package/hooks/dist/qgsd-spec-regen.js +86 -0
- package/hooks/dist/qgsd-statusline.js +91 -0
- package/hooks/dist/qgsd-stop.js +553 -0
- package/hooks/dist/qgsd-token-collector.js +133 -0
- package/hooks/dist/unified-mcp-server.mjs +669 -0
- package/package.json +95 -0
- package/scripts/build-hooks.js +46 -0
- package/scripts/postinstall.js +48 -0
- package/scripts/secret-audit.sh +45 -0
- package/templates/qgsd.json +49 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* validate-memory.cjs
|
|
6
|
+
*
|
|
7
|
+
* Memory staleness checker for QGSD auto-memory (MEMORY.md).
|
|
8
|
+
*
|
|
9
|
+
* Checks:
|
|
10
|
+
* 1. Stale counts — numbers referencing requirements.json count vs actual
|
|
11
|
+
* 2. Dead file references — file paths mentioned that don't exist on disk
|
|
12
|
+
* 3. Temporal markers — entries with "as of", dates, version-specific milestones
|
|
13
|
+
* 4. Contradiction with requirements — memory claims conflicting with current state
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node bin/validate-memory.cjs [--memory=path] [--cwd=path] [--quiet]
|
|
17
|
+
*
|
|
18
|
+
* Outputs findings to stderr (so they appear in Claude's context from hooks).
|
|
19
|
+
* Exits 0 always (never blocks session start).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// Find MEMORY.md
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function findMemoryPath(cwd) {
|
|
31
|
+
// Claude Code auto-memory path: ~/.claude/projects/<escaped-cwd>/memory/MEMORY.md
|
|
32
|
+
const escaped = cwd.replace(/\//g, '-');
|
|
33
|
+
const candidates = [
|
|
34
|
+
path.join(os.homedir(), '.claude', 'projects', escaped, 'memory', 'MEMORY.md'),
|
|
35
|
+
// Also try without leading dash
|
|
36
|
+
path.join(os.homedir(), '.claude', 'projects', escaped.replace(/^-/, ''), 'memory', 'MEMORY.md'),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
for (const p of candidates) {
|
|
40
|
+
if (fs.existsSync(p)) return p;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
// Check 1: Stale counts
|
|
47
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
function checkStaleCounts(memoryContent, cwd) {
|
|
50
|
+
const findings = [];
|
|
51
|
+
|
|
52
|
+
// Check requirements count references
|
|
53
|
+
const reqCountMatch = memoryContent.match(/(\d+)\s+reqs?/i);
|
|
54
|
+
if (reqCountMatch) {
|
|
55
|
+
const claimedCount = parseInt(reqCountMatch[1], 10);
|
|
56
|
+
const envelopePath = path.join(cwd, '.planning', 'formal', 'requirements.json');
|
|
57
|
+
if (fs.existsSync(envelopePath)) {
|
|
58
|
+
try {
|
|
59
|
+
const envelope = JSON.parse(fs.readFileSync(envelopePath, 'utf8'));
|
|
60
|
+
const actualCount = (envelope.requirements || []).length;
|
|
61
|
+
if (claimedCount !== actualCount) {
|
|
62
|
+
findings.push({
|
|
63
|
+
type: 'stale_count',
|
|
64
|
+
message: `Requirements count: memory says "${claimedCount} reqs" but envelope has ${actualCount}`,
|
|
65
|
+
fix: `Update "${claimedCount} reqs" → "${actualCount} reqs" in MEMORY.md`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
} catch (_) {}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check category group count references.
|
|
73
|
+
// category-groups.json is a flat { rawCategory: groupName } map.
|
|
74
|
+
// "N category groups" refers to unique target group names (values), not raw categories (keys).
|
|
75
|
+
const catCountMatch = memoryContent.match(/(\d+)\s+category\s+groups?/i);
|
|
76
|
+
if (catCountMatch) {
|
|
77
|
+
const claimedCats = parseInt(catCountMatch[1], 10);
|
|
78
|
+
const catPath = path.join(cwd, '.planning', 'formal', 'category-groups.json');
|
|
79
|
+
if (fs.existsSync(catPath)) {
|
|
80
|
+
try {
|
|
81
|
+
const cats = JSON.parse(fs.readFileSync(catPath, 'utf8'));
|
|
82
|
+
const mapping = cats.groups || cats;
|
|
83
|
+
const uniqueGroups = new Set(
|
|
84
|
+
Object.entries(mapping)
|
|
85
|
+
.filter(([k]) => !k.startsWith('_'))
|
|
86
|
+
.map(([, v]) => v)
|
|
87
|
+
);
|
|
88
|
+
const actualGroups = uniqueGroups.size;
|
|
89
|
+
if (claimedCats !== actualGroups) {
|
|
90
|
+
findings.push({
|
|
91
|
+
type: 'stale_count',
|
|
92
|
+
message: `Category groups: memory says "${claimedCats}" but file has ${actualGroups} unique groups`,
|
|
93
|
+
fix: `Update "${claimedCats}" → "${actualGroups}" in MEMORY.md`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
} catch (_) {}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check category mapping references (e.g., "66→9")
|
|
101
|
+
const mappingMatch = memoryContent.match(/(\d+)→(\d+)\s+category\s+mapping/i);
|
|
102
|
+
if (mappingMatch) {
|
|
103
|
+
const claimedFrom = parseInt(mappingMatch[1], 10);
|
|
104
|
+
const claimedTo = parseInt(mappingMatch[2], 10);
|
|
105
|
+
const catPath = path.join(cwd, '.planning', 'formal', 'category-groups.json');
|
|
106
|
+
if (fs.existsSync(catPath)) {
|
|
107
|
+
try {
|
|
108
|
+
const cats = JSON.parse(fs.readFileSync(catPath, 'utf8'));
|
|
109
|
+
const mapping = cats.groups || cats;
|
|
110
|
+
const rawEntries = Object.keys(mapping).filter(k => !k.startsWith('_'));
|
|
111
|
+
const uniqueGroups = new Set(rawEntries.map(k => mapping[k]));
|
|
112
|
+
const actualFrom = rawEntries.length;
|
|
113
|
+
const actualTo = uniqueGroups.size;
|
|
114
|
+
if (actualFrom !== claimedFrom || actualTo !== claimedTo) {
|
|
115
|
+
findings.push({
|
|
116
|
+
type: 'stale_count',
|
|
117
|
+
message: `Category mapping: memory says "${claimedFrom}→${claimedTo}" but actual is ${actualFrom}→${actualTo}`,
|
|
118
|
+
fix: `Update mapping count in MEMORY.md`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
} catch (_) {}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return findings;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
129
|
+
// Check 2: Dead file references
|
|
130
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
function checkDeadFileRefs(memoryContent, cwd) {
|
|
133
|
+
const findings = [];
|
|
134
|
+
|
|
135
|
+
// Match file paths in backticks: `path/to/file` or `path/to/file.ext`
|
|
136
|
+
const pathPattern = /`([a-zA-Z0-9_./-]+\.[a-zA-Z]+)`/g;
|
|
137
|
+
const dirPattern = /`([a-zA-Z0-9_./-]+\/)`/g;
|
|
138
|
+
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
|
|
141
|
+
for (const pattern of [pathPattern, dirPattern]) {
|
|
142
|
+
let match;
|
|
143
|
+
while ((match = pattern.exec(memoryContent)) !== null) {
|
|
144
|
+
const ref = match[1];
|
|
145
|
+
if (seen.has(ref)) continue;
|
|
146
|
+
seen.add(ref);
|
|
147
|
+
|
|
148
|
+
// Skip URLs, patterns, and common non-path references
|
|
149
|
+
if (ref.includes('://') || ref.includes('*') || ref.includes('{')) continue;
|
|
150
|
+
// Skip package names like qgsd@0.2.0
|
|
151
|
+
if (ref.includes('@')) continue;
|
|
152
|
+
// Skip references that start with ~ (home dir)
|
|
153
|
+
if (ref.startsWith('~')) {
|
|
154
|
+
const expanded = ref.replace('~', os.homedir());
|
|
155
|
+
if (!fs.existsSync(expanded)) {
|
|
156
|
+
findings.push({
|
|
157
|
+
type: 'dead_ref',
|
|
158
|
+
message: `File reference not found: \`${ref}\``,
|
|
159
|
+
fix: `Remove or update the reference to \`${ref}\` in MEMORY.md`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check relative to cwd
|
|
166
|
+
const fullPath = path.join(cwd, ref);
|
|
167
|
+
if (!fs.existsSync(fullPath)) {
|
|
168
|
+
findings.push({
|
|
169
|
+
type: 'dead_ref',
|
|
170
|
+
message: `File reference not found: \`${ref}\``,
|
|
171
|
+
fix: `Remove or update the reference to \`${ref}\` in MEMORY.md`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return findings;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
181
|
+
// Check 3: Temporal markers
|
|
182
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
function checkTemporalMarkers(memoryContent) {
|
|
185
|
+
const findings = [];
|
|
186
|
+
const lines = memoryContent.split('\n');
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i < lines.length; i++) {
|
|
189
|
+
const line = lines[i];
|
|
190
|
+
const lineNum = i + 1;
|
|
191
|
+
|
|
192
|
+
// "as of YYYY-MM-DD" or "as of 2026-02-22"
|
|
193
|
+
const asOfMatch = line.match(/as\s+of\s+(\d{4}-\d{2}-\d{2}|\w+\s+\d{4})/i);
|
|
194
|
+
if (asOfMatch) {
|
|
195
|
+
findings.push({
|
|
196
|
+
type: 'temporal',
|
|
197
|
+
message: `Line ${lineNum}: temporal marker "as of ${asOfMatch[1]}" — may be outdated`,
|
|
198
|
+
fix: `Verify if the information on line ${lineNum} is still current`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// "until <date>" pattern
|
|
203
|
+
const untilMatch = line.match(/until\s+(\w+\s+\d{1,2}\s+\d{4}|\d{4}-\d{2}-\d{2})/i);
|
|
204
|
+
if (untilMatch) {
|
|
205
|
+
findings.push({
|
|
206
|
+
type: 'temporal',
|
|
207
|
+
message: `Line ${lineNum}: expiry marker "until ${untilMatch[1]}" — may have passed`,
|
|
208
|
+
fix: `Check if "until ${untilMatch[1]}" has passed and update accordingly`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Specific version-milestone patterns that may be outdated
|
|
213
|
+
const milestoneMatch = line.match(/Current\s+milestone:\s*(.+)/i);
|
|
214
|
+
if (milestoneMatch) {
|
|
215
|
+
findings.push({
|
|
216
|
+
type: 'temporal',
|
|
217
|
+
message: `Line ${lineNum}: milestone reference "${milestoneMatch[1].trim()}" — verify still current`,
|
|
218
|
+
fix: `Check .planning/ROADMAP.md or PROJECT.md for actual current milestone`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return findings;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
227
|
+
// Check 4: Contradiction with requirements
|
|
228
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
function checkContradictions(memoryContent, cwd) {
|
|
231
|
+
const findings = [];
|
|
232
|
+
|
|
233
|
+
const envelopePath = path.join(cwd, '.planning', 'formal', 'requirements.json');
|
|
234
|
+
if (!fs.existsSync(envelopePath)) return findings;
|
|
235
|
+
|
|
236
|
+
let envelope;
|
|
237
|
+
try {
|
|
238
|
+
envelope = JSON.parse(fs.readFileSync(envelopePath, 'utf8'));
|
|
239
|
+
} catch (_) {
|
|
240
|
+
return findings;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const requirements = envelope.requirements || [];
|
|
244
|
+
|
|
245
|
+
// Check if memory references CLAUDE.md but it doesn't exist
|
|
246
|
+
if (memoryContent.includes('CLAUDE.md')) {
|
|
247
|
+
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
248
|
+
if (!fs.existsSync(claudeMdPath)) {
|
|
249
|
+
findings.push({
|
|
250
|
+
type: 'contradiction',
|
|
251
|
+
message: 'Memory references CLAUDE.md but file does not exist in repo',
|
|
252
|
+
fix: 'Remove or update CLAUDE.md references in MEMORY.md',
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Check if memory mentions specific requirement IDs that no longer exist
|
|
258
|
+
const idPattern = /\b([A-Z]+-\d+)\b/g;
|
|
259
|
+
let match;
|
|
260
|
+
const reqIds = new Set(requirements.map(r => r.id));
|
|
261
|
+
const mentionedIds = new Set();
|
|
262
|
+
|
|
263
|
+
while ((match = idPattern.exec(memoryContent)) !== null) {
|
|
264
|
+
const id = match[1];
|
|
265
|
+
// Only check IDs that look like requirement IDs (not arbitrary uppercase patterns)
|
|
266
|
+
if (/^(ACT|AGENT|BREAKER|BLD|CL|CONF|DASH|DIAG|ENFC|HOOK|IMPR|MCP|OBS|QUICK|REN|RLS|SAFE|SCHEMA|SLOT|SPEC|STATE|STD|SYNC|VERIFY)-\d+$/.test(id)) {
|
|
267
|
+
if (!reqIds.has(id) && !mentionedIds.has(id)) {
|
|
268
|
+
mentionedIds.add(id);
|
|
269
|
+
findings.push({
|
|
270
|
+
type: 'contradiction',
|
|
271
|
+
message: `Memory references requirement ${id} which is not in the current envelope`,
|
|
272
|
+
fix: `Verify if ${id} was archived or renamed, and update MEMORY.md`,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return findings;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
282
|
+
// Main
|
|
283
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
function validateMemory(options = {}) {
|
|
286
|
+
const { cwd = process.cwd(), memoryPath = null, quiet = false } = options;
|
|
287
|
+
|
|
288
|
+
// Find memory file
|
|
289
|
+
const resolvedMemoryPath = memoryPath || findMemoryPath(cwd);
|
|
290
|
+
if (!resolvedMemoryPath || !fs.existsSync(resolvedMemoryPath)) {
|
|
291
|
+
if (!quiet) process.stderr.write('[validate-memory] No MEMORY.md found — skipping\n');
|
|
292
|
+
return { findings: [], memoryPath: null };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const content = fs.readFileSync(resolvedMemoryPath, 'utf8');
|
|
296
|
+
|
|
297
|
+
const allFindings = [
|
|
298
|
+
...checkStaleCounts(content, cwd),
|
|
299
|
+
...checkDeadFileRefs(content, cwd),
|
|
300
|
+
...checkTemporalMarkers(content),
|
|
301
|
+
...checkContradictions(content, cwd),
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
return { findings: allFindings, memoryPath: resolvedMemoryPath };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
308
|
+
// CLI entrypoint
|
|
309
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
function main() {
|
|
312
|
+
const args = {};
|
|
313
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
314
|
+
const arg = process.argv[i];
|
|
315
|
+
if (arg.startsWith('--')) {
|
|
316
|
+
const eqIdx = arg.indexOf('=');
|
|
317
|
+
if (eqIdx !== -1) {
|
|
318
|
+
args[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
|
|
319
|
+
} else {
|
|
320
|
+
args[arg.slice(2)] = true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const cwd = args.cwd || process.cwd();
|
|
326
|
+
const memoryPath = args.memory || null;
|
|
327
|
+
const quiet = !!args.quiet;
|
|
328
|
+
|
|
329
|
+
const { findings, memoryPath: resolvedPath } = validateMemory({ cwd, memoryPath, quiet });
|
|
330
|
+
|
|
331
|
+
if (!resolvedPath) {
|
|
332
|
+
process.exit(0);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (findings.length === 0) {
|
|
336
|
+
if (!quiet) console.log('Memory validation: all checks passed');
|
|
337
|
+
process.exit(0);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Group by type
|
|
341
|
+
const grouped = {};
|
|
342
|
+
for (const f of findings) {
|
|
343
|
+
if (!grouped[f.type]) grouped[f.type] = [];
|
|
344
|
+
grouped[f.type].push(f);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const typeLabels = {
|
|
348
|
+
stale_count: 'Stale Counts',
|
|
349
|
+
dead_ref: 'Dead File References',
|
|
350
|
+
temporal: 'Temporal Markers',
|
|
351
|
+
contradiction: 'Contradictions',
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
console.log(`\nMemory Validation — ${findings.length} finding(s):\n`);
|
|
355
|
+
|
|
356
|
+
for (const [type, items] of Object.entries(grouped)) {
|
|
357
|
+
console.log(` ${typeLabels[type] || type} (${items.length}):`);
|
|
358
|
+
for (const item of items) {
|
|
359
|
+
console.log(` - ${item.message}`);
|
|
360
|
+
console.log(` Fix: ${item.fix}`);
|
|
361
|
+
}
|
|
362
|
+
console.log('');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Also write to stderr for hook integration
|
|
366
|
+
if (!quiet) {
|
|
367
|
+
const summary = findings.map(f => `[memory] ${f.message}`).join('\n');
|
|
368
|
+
process.stderr.write(summary + '\n');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
process.exit(0);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
375
|
+
// Exports
|
|
376
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
module.exports = {
|
|
379
|
+
validateMemory,
|
|
380
|
+
findMemoryPath,
|
|
381
|
+
checkStaleCounts,
|
|
382
|
+
checkDeadFileRefs,
|
|
383
|
+
checkTemporalMarkers,
|
|
384
|
+
checkContradictions,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
if (require.main === module) {
|
|
388
|
+
main();
|
|
389
|
+
}
|