claudeos-core 2.0.2 → 2.1.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/CHANGELOG.md +208 -0
- package/README.de.md +961 -880
- package/README.es.md +960 -880
- package/README.fr.md +960 -880
- package/README.hi.md +960 -880
- package/README.ja.md +960 -880
- package/README.ko.md +126 -47
- package/README.md +126 -46
- package/README.ru.md +960 -880
- package/README.vi.md +128 -48
- package/README.zh-CN.md +959 -880
- package/bin/cli.js +7 -2
- package/bin/commands/init.js +733 -143
- package/bin/commands/memory.js +17 -5
- package/bootstrap.sh +81 -81
- package/lib/expected-outputs.js +6 -7
- package/lib/memory-scaffold.js +84 -46
- package/lib/plan-parser.js +12 -0
- package/manifest-generator/index.js +16 -18
- package/package.json +1 -1
- package/pass-prompts/templates/angular/pass3.md +2 -10
- package/pass-prompts/templates/common/pass3-phase1.md +131 -0
- package/pass-prompts/templates/common/pass3a-facts.md +143 -0
- package/pass-prompts/templates/common/pass3b-core-header.md +58 -0
- package/pass-prompts/templates/common/pass3c-skills-guide-header.md +53 -0
- package/pass-prompts/templates/common/pass3d-plan-aux-header.md +57 -0
- package/pass-prompts/templates/common/pass4.md +4 -19
- package/pass-prompts/templates/java-spring/pass3.md +5 -15
- package/pass-prompts/templates/kotlin-spring/pass3.md +5 -15
- package/pass-prompts/templates/node-express/pass3.md +5 -14
- package/pass-prompts/templates/node-fastify/pass3.md +2 -10
- package/pass-prompts/templates/node-nestjs/pass3.md +5 -13
- package/pass-prompts/templates/node-nextjs/pass3.md +5 -14
- package/pass-prompts/templates/node-vite/pass3.md +95 -103
- package/pass-prompts/templates/python-django/pass3.md +5 -14
- package/pass-prompts/templates/python-fastapi/pass3.md +5 -14
- package/pass-prompts/templates/python-flask/pass3.md +95 -103
- package/pass-prompts/templates/vue-nuxt/pass3.md +2 -10
- package/plan-installer/pass3-context-builder.js +258 -0
- package/plan-installer/prompt-generator.js +9 -1
- package/plan-validator/index.js +23 -8
- package/sync-checker/index.js +44 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeOS-Core — Pass 3 Context Builder
|
|
3
|
+
*
|
|
4
|
+
* Builds a slim `pass3-context.json` that Pass 3 prompts reference INSTEAD OF
|
|
5
|
+
* re-reading the full `pass2-merged.json` repeatedly. pass2-merged.json on
|
|
6
|
+
* large multi-module projects can exceed 300 KB, and Pass 3 historically
|
|
7
|
+
* re-reads it once per file generated (30-50 times), which is the #1 cause
|
|
8
|
+
* of `Prompt is too long` Pass 3 failures.
|
|
9
|
+
*
|
|
10
|
+
* Design constraints:
|
|
11
|
+
* - pass2-merged.json is an LLM-generated free-form JSON: field names and
|
|
12
|
+
* nesting vary by stack and by Claude's interpretation of the pass2
|
|
13
|
+
* template. We CANNOT reliably project specific nested fields out of it.
|
|
14
|
+
* - project-analysis.json IS structured (we wrote it ourselves in
|
|
15
|
+
* plan-installer). We use it as the authoritative source for stack facts.
|
|
16
|
+
* - pass2-merged.json is still useful as a file, so we report its
|
|
17
|
+
* existence/size/top-level key count here as signals — the Pass 3 prompt
|
|
18
|
+
* then knows whether to fall back to reading it for specific details.
|
|
19
|
+
*
|
|
20
|
+
* Output shape (see `buildPass3Context` return) is intentionally flat and
|
|
21
|
+
* small: <5 KB even for large projects, vs. pass2-merged.json at 50-500 KB.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
"use strict";
|
|
25
|
+
|
|
26
|
+
const fs = require("fs");
|
|
27
|
+
const path = require("path");
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Safely read + parse a JSON file. Returns null on any error.
|
|
31
|
+
* Mirrors lib/safe-fs.js readJsonSafe but inlined to avoid circular-dep risk
|
|
32
|
+
* during tool-time module loading (this file is called from plan-installer
|
|
33
|
+
* after all scanners have run).
|
|
34
|
+
*/
|
|
35
|
+
function readJsonSafe(filePath) {
|
|
36
|
+
try {
|
|
37
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
38
|
+
return JSON.parse(raw);
|
|
39
|
+
} catch (_e) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Describe pass2-merged.json as signals (size, top-level key count, existence)
|
|
46
|
+
* without loading its contents into the context JSON. Pass 3 prompts use these
|
|
47
|
+
* signals to decide whether to read the full file for a specific missing detail.
|
|
48
|
+
*/
|
|
49
|
+
function describePass2(pass2MergedPath) {
|
|
50
|
+
let exists = false;
|
|
51
|
+
let sizeBytes = 0;
|
|
52
|
+
let topLevelKeys = [];
|
|
53
|
+
try {
|
|
54
|
+
const stat = fs.statSync(pass2MergedPath);
|
|
55
|
+
exists = true;
|
|
56
|
+
sizeBytes = stat.size;
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(fs.readFileSync(pass2MergedPath, "utf-8"));
|
|
59
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
60
|
+
topLevelKeys = Object.keys(parsed).slice(0, 40); // cap at 40 to keep context small
|
|
61
|
+
}
|
|
62
|
+
} catch (_e) {
|
|
63
|
+
// Malformed pass2-merged.json — leave topLevelKeys empty, still report size.
|
|
64
|
+
}
|
|
65
|
+
} catch (_e) {
|
|
66
|
+
// File doesn't exist yet (e.g. Pass 2 has not run). Still return a valid descriptor.
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
exists,
|
|
70
|
+
sizeBytes,
|
|
71
|
+
sizeKB: Math.round(sizeBytes / 1024),
|
|
72
|
+
topLevelKeys,
|
|
73
|
+
// Heuristic flag: >300 KB is the empirical threshold above which repeated
|
|
74
|
+
// re-reads reliably cause Pass 3 context overflow on 200K-context models.
|
|
75
|
+
large: sizeBytes > 300 * 1024,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extract domain summaries from project-analysis.json's domains array.
|
|
81
|
+
* Each entry in `domains` has varying fields depending on the scanner that
|
|
82
|
+
* produced it (scan-java, scan-kotlin, scan-node, scan-python, scan-frontend).
|
|
83
|
+
* We project only the fields that are universally useful for Pass 3 consistency.
|
|
84
|
+
*/
|
|
85
|
+
function summarizeDomains(analysisDomains) {
|
|
86
|
+
if (!Array.isArray(analysisDomains)) return [];
|
|
87
|
+
return analysisDomains.map((d) => {
|
|
88
|
+
const summary = {
|
|
89
|
+
name: d.name,
|
|
90
|
+
type: d.type || "backend",
|
|
91
|
+
totalFiles: d.totalFiles || 0,
|
|
92
|
+
};
|
|
93
|
+
// Optional, scanner-specific fields — only include if present.
|
|
94
|
+
if (typeof d.controllers === "number") summary.controllers = d.controllers;
|
|
95
|
+
if (typeof d.services === "number") summary.services = d.services;
|
|
96
|
+
if (typeof d.repositories === "number") summary.repositories = d.repositories;
|
|
97
|
+
if (typeof d.mappers === "number") summary.mappers = d.mappers;
|
|
98
|
+
if (typeof d.dtos === "number") summary.dtos = d.dtos;
|
|
99
|
+
if (typeof d.pages === "number") summary.pages = d.pages;
|
|
100
|
+
if (typeof d.components === "number") summary.components = d.components;
|
|
101
|
+
if (d.pattern) summary.pattern = d.pattern; // Java A-E patterns
|
|
102
|
+
if (d.modulePath) summary.modulePath = d.modulePath; // multi-module Java
|
|
103
|
+
if (d.serverType) summary.serverType = d.serverType; // Kotlin CQRS server types
|
|
104
|
+
if (d.domainName) summary.domainName = d.domainName;
|
|
105
|
+
return summary;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Extract the port default from project-analysis.stack.port (plan-installer
|
|
111
|
+
* sets this based on framework). Kept as a top-level convenience field so
|
|
112
|
+
* Pass 3 doesn't need to traverse the nested stack object.
|
|
113
|
+
*/
|
|
114
|
+
function extractPort(projectAnalysis) {
|
|
115
|
+
return (projectAnalysis && projectAnalysis.stack && projectAnalysis.stack.port) || null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build the slim Pass 3 context object.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} generatedDir - absolute path to claudeos-core/generated/
|
|
122
|
+
* @returns {object|null} context object, or null if project-analysis.json is
|
|
123
|
+
* missing/malformed (caller should then NOT write pass3-context.json — Pass 3
|
|
124
|
+
* will fall back to the full pass2-merged.json path).
|
|
125
|
+
*/
|
|
126
|
+
function buildPass3Context(generatedDir) {
|
|
127
|
+
const analysisPath = path.join(generatedDir, "project-analysis.json");
|
|
128
|
+
const pass2Path = path.join(generatedDir, "pass2-merged.json");
|
|
129
|
+
|
|
130
|
+
const analysis = readJsonSafe(analysisPath);
|
|
131
|
+
if (!analysis || typeof analysis !== "object") {
|
|
132
|
+
// Without project-analysis.json we have nothing structured to summarize.
|
|
133
|
+
// Let Pass 3 fall back to reading pass2-merged.json directly.
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const stack = analysis.stack || {};
|
|
138
|
+
const summary = analysis.summary || {};
|
|
139
|
+
const active = analysis.activeDomains || {};
|
|
140
|
+
|
|
141
|
+
const context = {
|
|
142
|
+
_schemaVersion: 1,
|
|
143
|
+
_generatedAt: new Date().toISOString(),
|
|
144
|
+
_purpose:
|
|
145
|
+
"Slim, read-once summary for Pass 3 prompts. Reference this INSTEAD OF " +
|
|
146
|
+
"re-reading pass2-merged.json during file generation. Only consult " +
|
|
147
|
+
"pass2-merged.json for specific method names / exact paths not captured " +
|
|
148
|
+
"here — and only once per detail.",
|
|
149
|
+
|
|
150
|
+
// ─── Stack facts (authoritative — from project-analysis.json) ──────
|
|
151
|
+
stack: {
|
|
152
|
+
language: stack.language || null,
|
|
153
|
+
languageVersion: stack.languageVersion || null,
|
|
154
|
+
framework: stack.framework || null,
|
|
155
|
+
frameworkVersion: stack.frameworkVersion || null,
|
|
156
|
+
buildTool: stack.buildTool || null,
|
|
157
|
+
packageManager: stack.packageManager || null,
|
|
158
|
+
database: stack.database || null,
|
|
159
|
+
orm: stack.orm || null,
|
|
160
|
+
frontend: stack.frontend || null,
|
|
161
|
+
frontendVersion: stack.frontendVersion || null,
|
|
162
|
+
port: extractPort(analysis),
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// ─── Architecture flags ──────────────────────────────────────────
|
|
166
|
+
architecture: {
|
|
167
|
+
cqrs: !!(stack.architecture && String(stack.architecture).includes("cqrs")),
|
|
168
|
+
bff: Array.isArray(stack.detected) && stack.detected.includes("bff"),
|
|
169
|
+
multiModule: !!stack.multiModule,
|
|
170
|
+
monorepo: stack.monorepo || null,
|
|
171
|
+
workspaces: stack.workspaces || null,
|
|
172
|
+
modules: Array.isArray(stack.modules) ? stack.modules : [],
|
|
173
|
+
rootPackage: analysis.rootPackage || null,
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
// ─── Active domain groups (which 00.core / 10.backend / ... apply) ──
|
|
177
|
+
activeDomains: active,
|
|
178
|
+
|
|
179
|
+
// ─── Template routing (what pass3 templates were combined) ──────────
|
|
180
|
+
templates: analysis.templates || {},
|
|
181
|
+
isMultiStack: !!analysis.isMultiStack,
|
|
182
|
+
|
|
183
|
+
// ─── Domain summary (flat list, small per-domain footprint) ─────────
|
|
184
|
+
domainCount: {
|
|
185
|
+
total: summary.totalDomains || 0,
|
|
186
|
+
backend: summary.backendDomains || 0,
|
|
187
|
+
frontend: summary.frontendDomains || 0,
|
|
188
|
+
},
|
|
189
|
+
backendDomains: summarizeDomains(analysis.backendDomains),
|
|
190
|
+
frontendDomains: summarizeDomains(analysis.frontendDomains),
|
|
191
|
+
|
|
192
|
+
// ─── Frontend stats (if scan-frontend ran) ──────────────────────────
|
|
193
|
+
frontend: analysis.frontend || { exists: false },
|
|
194
|
+
|
|
195
|
+
// ─── pass2-merged.json descriptor (signals, not contents) ───────────
|
|
196
|
+
// Pass 3 reads this to decide whether it's safe to open the full file
|
|
197
|
+
// for a specific missing detail, and how aggressively to summarize first.
|
|
198
|
+
pass2Merged: describePass2(pass2Path),
|
|
199
|
+
|
|
200
|
+
// ─── Split-mode recommendation (informational only) ─────────────────
|
|
201
|
+
// Heuristic that predicts whether single-call Pass 3 would likely hit
|
|
202
|
+
// `Prompt is too long`. Surfaced in init logs as "estimated N files
|
|
203
|
+
// from M domains". As of the current release this does NOT drive the
|
|
204
|
+
// split/single-call decision — init.js defaults to split mode for all
|
|
205
|
+
// projects regardless of this recommendation. Kept for diagnostic
|
|
206
|
+
// visibility and future use (e.g. token budget estimation, UI hints).
|
|
207
|
+
splitRecommendation: (function computeSplit() {
|
|
208
|
+
const pass2Desc = describePass2(pass2Path);
|
|
209
|
+
const backendCount = Array.isArray(analysis.backendDomains) ? analysis.backendDomains.length : 0;
|
|
210
|
+
const frontendCount = Array.isArray(analysis.frontendDomains) ? analysis.frontendDomains.length : 0;
|
|
211
|
+
const totalDomains = backendCount + frontendCount;
|
|
212
|
+
|
|
213
|
+
// Rough estimate: each domain yields 5-8 files (standards + rules + skills),
|
|
214
|
+
// plus ~15 stack-independent files (CLAUDE.md, guides, master plans, etc.).
|
|
215
|
+
const estimatedFileCount = 15 + totalDomains * 6;
|
|
216
|
+
|
|
217
|
+
// Thresholds calibrated from empirical failures:
|
|
218
|
+
// - pass2 > 300KB: historic input-overflow threshold (v2.1 original heuristic)
|
|
219
|
+
// - estimated files > 35: empirical output-accumulation threshold
|
|
220
|
+
// (observed overflow at ~40 files with pass2=35KB — output was the cause)
|
|
221
|
+
const largeInput = pass2Desc.sizeBytes > 300 * 1024;
|
|
222
|
+
const largeOutput = estimatedFileCount > 35;
|
|
223
|
+
const recommend = largeInput || largeOutput;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
recommend,
|
|
227
|
+
reasons: [
|
|
228
|
+
largeInput ? `pass2-merged.json is ${pass2Desc.sizeKB} KB (> 300 KB input threshold)` : null,
|
|
229
|
+
largeOutput ? `estimated ${estimatedFileCount} output files (> 35 file threshold) from ${totalDomains} domains` : null,
|
|
230
|
+
].filter(Boolean),
|
|
231
|
+
estimatedFileCount,
|
|
232
|
+
totalDomains,
|
|
233
|
+
};
|
|
234
|
+
})(),
|
|
235
|
+
|
|
236
|
+
// ─── Fields that pass2-merged.json is authoritative for ─────────────
|
|
237
|
+
// These are all `null` in pass3-context.json by design — Pass 3 must
|
|
238
|
+
// read pass2-merged.json ONCE (during the Phase 1 fact-table fill)
|
|
239
|
+
// to get their actual values. Listed here so Pass 3 knows exactly
|
|
240
|
+
// what to look for instead of free-form scanning.
|
|
241
|
+
needsPass2: {
|
|
242
|
+
responseWrapperLayer: null, // "Controller" | "Aggregator" | "Service"
|
|
243
|
+
responseWrapperMethod: null, // e.g. "makeResponse"
|
|
244
|
+
responseUtilClass: null, // FQN, e.g. "com.company.util.ApiResponseUtil"
|
|
245
|
+
responseUtilMethods: null, // ["success", "fail", ...] exact names
|
|
246
|
+
mapperXmlPath: null, // verbatim, if MyBatis
|
|
247
|
+
aggregatorExists: null,
|
|
248
|
+
aggregatorNaming: null,
|
|
249
|
+
sharedBaseClasses: null, // array of FQNs
|
|
250
|
+
sharedUtilPackage: null,
|
|
251
|
+
authMethod: null, // "JWT" | "Session" | "OAuth2" | ...
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
return context;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = { buildPass3Context, describePass2, summarizeDomains };
|
|
@@ -28,6 +28,14 @@ function generatePrompts(templates, lang, templatesDir, generatedDir) {
|
|
|
28
28
|
// claudeos-core/generated/.staged-rules/* to bypass Claude Code's sensitive-
|
|
29
29
|
// path block. The Node.js orchestrator moves the staged files after each pass.
|
|
30
30
|
const stagingOverride = existsSafe(stagingOverridePath) ? readFileSafe(stagingOverridePath) + "\n" : "";
|
|
31
|
+
// v2.1: Phase 1 "Read Once, Extract Facts" block prepended to every Pass 3
|
|
32
|
+
// prompt. Teaches Claude to read pass2-merged.json exactly once into a
|
|
33
|
+
// compact in-context fact table and reference that table for all subsequent
|
|
34
|
+
// file generation — fixes the `Prompt is too long` failure on large projects
|
|
35
|
+
// caused by 10-20× re-reads of pass2-merged.json. Also includes idempotent
|
|
36
|
+
// skip rules (Rule B) so interrupted Pass 3 runs can resume safely.
|
|
37
|
+
const phase1Path = path.join(commonDir, "pass3-phase1.md");
|
|
38
|
+
const phase1 = existsSafe(phase1Path) ? readFileSafe(phase1Path) + "\n" : "";
|
|
31
39
|
|
|
32
40
|
let langInstruction = "";
|
|
33
41
|
if (lang && lang !== "en" && existsSafe(langPath)) {
|
|
@@ -92,7 +100,7 @@ function generatePrompts(templates, lang, templatesDir, generatedDir) {
|
|
|
92
100
|
|
|
93
101
|
writeFileSafe(
|
|
94
102
|
path.join(generatedDir, "pass3-prompt.md"),
|
|
95
|
-
header + langInstruction + stagingOverride + combinedBody.trimEnd() + "\n" + footer
|
|
103
|
+
header + langInstruction + stagingOverride + phase1 + combinedBody.trimEnd() + "\n" + footer
|
|
96
104
|
);
|
|
97
105
|
console.log(` ✅ pass3-prompt.md${templates.frontend && templates.backend ? " (multi-stack combined)" : ""}`);
|
|
98
106
|
}
|
package/plan-validator/index.js
CHANGED
|
@@ -57,8 +57,14 @@ async function main() {
|
|
|
57
57
|
console.log("╚═══════════════════════════════════════╝\n");
|
|
58
58
|
|
|
59
59
|
if (!fs.existsSync(PLAN)) {
|
|
60
|
-
console.log("
|
|
61
|
-
|
|
60
|
+
console.log(" ℹ️ Plan directory not present — nothing to validate.");
|
|
61
|
+
console.log(" (Master plans are no longer generated by default; this is normal.)\n");
|
|
62
|
+
// Emit empty stale-report block so health-checker summary is consistent.
|
|
63
|
+
updateStaleReport(GEN, "planValidation",
|
|
64
|
+
{ checkedAt: new Date().toISOString(), mode, total: 0, synced: 0, drift: 0, missing: 0 },
|
|
65
|
+
{ planDrift: 0, planMissing: 0 }
|
|
66
|
+
);
|
|
67
|
+
process.exit(0);
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
let total = 0, synced = 0, drift = 0, missing = 0;
|
|
@@ -145,13 +151,22 @@ async function main() {
|
|
|
145
151
|
console.log(drift + missing === 0 ? " ✅ All plans in sync\n" : ` ⚠️ ${drift + missing} issues\n`);
|
|
146
152
|
|
|
147
153
|
// ─── Update plan-sync-status.json + stale-report.json ──
|
|
154
|
+
// v2.1.0: master plan generation was removed. When plan/ exists but contains
|
|
155
|
+
// no .md files (legacy directory skeleton), we skip plan-sync-status.json
|
|
156
|
+
// entirely — writing a 147 B file full of zeros is noise. stale-report still
|
|
157
|
+
// records the (passing) validation run so health-checker sees a clean result.
|
|
158
|
+
// Note: plan/ missing entirely is already handled by the early-return at
|
|
159
|
+
// the top of main() (line ~59), so by here PLAN exists — but may be empty.
|
|
160
|
+
const planHasFiles = fs.readdirSync(PLAN).some(f => f.endsWith(".md"));
|
|
148
161
|
if (fs.existsSync(GEN)) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
162
|
+
if (planHasFiles) {
|
|
163
|
+
fs.writeFileSync(
|
|
164
|
+
path.join(GEN, "plan-sync-status.json"),
|
|
165
|
+
JSON.stringify({ generatedAt: new Date().toISOString(), lastMode: mode, total, synced, drift, missing, issues: results }, null, 2)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
// stale-report is always updated so health-checker sees a consistent
|
|
169
|
+
// planValidation entry regardless of plan/ contents.
|
|
155
170
|
updateStaleReport(GEN, "planValidation",
|
|
156
171
|
{ checkedAt: new Date().toISOString(), mode, total, synced, drift, missing },
|
|
157
172
|
{ planDrift: drift, planMissing: missing }
|
package/sync-checker/index.js
CHANGED
|
@@ -50,7 +50,27 @@ async function main() {
|
|
|
50
50
|
console.log("║ ClaudeOS-Core — Sync Checker ║");
|
|
51
51
|
console.log("╚═══════════════════════════════════════╝\n");
|
|
52
52
|
|
|
53
|
+
// Master plan directory is optional. If it doesn't exist (new default for
|
|
54
|
+
// claudeos-core, since master plans are no longer generated) AND sync-map
|
|
55
|
+
// has no mappings to validate, sync-checker has nothing to compare against
|
|
56
|
+
// and should skip cleanly. This is a PASS state, not a failure.
|
|
57
|
+
//
|
|
58
|
+
// However, if sync-map.json DOES contain mappings (either because master
|
|
59
|
+
// plans exist, or because a caller wrote mappings directly for testing),
|
|
60
|
+
// we still validate them normally.
|
|
61
|
+
const PLAN_DIR = path.join(ROOT, "claudeos-core/plan");
|
|
62
|
+
const planExists = fs.existsSync(PLAN_DIR);
|
|
63
|
+
|
|
53
64
|
if (!fs.existsSync(SMP)) {
|
|
65
|
+
// No sync-map at all.
|
|
66
|
+
if (!planExists) {
|
|
67
|
+
console.log(" ℹ️ No plan/ directory and no sync-map.json — nothing to compare; skipping.\n");
|
|
68
|
+
updateStaleReport(GEN, "syncMisses",
|
|
69
|
+
{ checkedAt: new Date().toISOString(), unregistered: [], orphaned: [], skipped: true },
|
|
70
|
+
{ syncIssues: 0, status: "ok" }
|
|
71
|
+
);
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
54
74
|
console.log(" ❌ sync-map.json not found. Run manifest-generator first.\n");
|
|
55
75
|
process.exit(1);
|
|
56
76
|
}
|
|
@@ -66,6 +86,30 @@ async function main() {
|
|
|
66
86
|
console.log(" ❌ sync-map.json has no mappings array.\n");
|
|
67
87
|
process.exit(1);
|
|
68
88
|
}
|
|
89
|
+
|
|
90
|
+
// If sync-map has no mappings AND plan/ directory doesn't exist, skip
|
|
91
|
+
// cleanly — there's no ground truth to validate against and no master plans
|
|
92
|
+
// in use.
|
|
93
|
+
if (sm.mappings.length === 0 && !planExists) {
|
|
94
|
+
console.log(" ℹ️ No plan/ directory and sync-map has no mappings — skipping.\n");
|
|
95
|
+
updateStaleReport(GEN, "syncMisses",
|
|
96
|
+
{ checkedAt: new Date().toISOString(), unregistered: [], orphaned: [], skipped: true },
|
|
97
|
+
{ syncIssues: 0, status: "ok" }
|
|
98
|
+
);
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If sync-map has no mappings but plan/ exists (e.g., empty plan files),
|
|
103
|
+
// skip without raising a warning — there's nothing to validate.
|
|
104
|
+
if (sm.mappings.length === 0) {
|
|
105
|
+
console.log(" ℹ️ sync-map has no mappings — nothing to validate; skipping.\n");
|
|
106
|
+
updateStaleReport(GEN, "syncMisses",
|
|
107
|
+
{ checkedAt: new Date().toISOString(), unregistered: [], orphaned: [], skipped: true },
|
|
108
|
+
{ syncIssues: 0, status: "ok" }
|
|
109
|
+
);
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
|
|
69
113
|
const reg = new Set(sm.mappings.map((m) => m.sourcePath).filter(Boolean));
|
|
70
114
|
const issues = { unreg: [], orphan: [] };
|
|
71
115
|
|