create-sdd-project 0.16.9 → 0.17.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/bin/cli.js +2 -0
- package/lib/adapt-agents.js +117 -63
- package/lib/doctor.js +221 -0
- package/lib/generator.js +8 -0
- package/lib/init-generator.js +121 -198
- package/lib/meta.js +291 -0
- package/lib/stack-adaptations.js +335 -0
- package/lib/upgrade-generator.js +441 -36
- package/package.json +1 -1
- package/template/.claude/agents/backend-planner.md +3 -3
- package/template/.claude/agents/frontend-planner.md +3 -3
- package/template/.gemini/agents/backend-planner.md +2 -2
- package/template/.gemini/agents/frontend-planner.md +3 -3
- package/template/gitignore +6 -0
package/bin/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ const isInit = args.includes('--init');
|
|
|
11
11
|
const isUpgrade = args.includes('--upgrade');
|
|
12
12
|
const isDoctor = args.includes('--doctor');
|
|
13
13
|
const isForce = args.includes('--force');
|
|
14
|
+
const isForceTemplate = args.includes('--force-template');
|
|
14
15
|
const isEject = args.includes('--eject');
|
|
15
16
|
const isDiff = args.includes('--diff');
|
|
16
17
|
|
|
@@ -185,6 +186,7 @@ async function runUpgrade() {
|
|
|
185
186
|
config.autonomyLevel = autonomy.level;
|
|
186
187
|
config.autonomyName = autonomy.name;
|
|
187
188
|
config.installedVersion = installedVersion;
|
|
189
|
+
config.forceTemplate = isForceTemplate;
|
|
188
190
|
|
|
189
191
|
// Build and show summary
|
|
190
192
|
const state = {
|
package/lib/adapt-agents.js
CHANGED
|
@@ -2,10 +2,114 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Per-agent-file adaptation rules for single-stack projects.
|
|
7
|
+
*
|
|
8
|
+
* For each projectType ('backend' or 'frontend'), maps filename to a list of
|
|
9
|
+
* [search, replace] tuples applied in order. When `search` is a RegExp, uses
|
|
10
|
+
* .replace(); when it's a string, uses split/join to replace all occurrences.
|
|
11
|
+
*
|
|
12
|
+
* These rules ONLY apply to files under .claude/agents/ or .gemini/agents/.
|
|
13
|
+
* Skill and template file adaptations live in the I/O wrapper below because
|
|
14
|
+
* they don't need the pure function (they're not used by upgrade smart-diff).
|
|
15
|
+
*
|
|
16
|
+
* fullstack projectType has no rules — templates ship in fullstack-ready form.
|
|
17
|
+
*/
|
|
18
|
+
const AGENT_ADAPTATION_RULES = {
|
|
19
|
+
backend: {
|
|
20
|
+
'spec-creator.md': [
|
|
21
|
+
[/### Frontend Specifications\n(?:- [^\n]*\n)+\n/, ''],
|
|
22
|
+
[/### For UI Changes\n```markdown\n(?:[^\n]*\n)*?```\n\n/, ''],
|
|
23
|
+
['Data Model Changes, UI Changes, Edge Cases', 'Data Model Changes, Edge Cases'],
|
|
24
|
+
['(`api-spec.yaml`, `ui-components.md`)', '(`api-spec.yaml`)'],
|
|
25
|
+
// Gemini agents have different text patterns (also applied to Claude; no-op if no match):
|
|
26
|
+
[/\(api-spec\.yaml, ui-components\.md\)/, '(api-spec.yaml)'],
|
|
27
|
+
],
|
|
28
|
+
'production-code-validator.md': [
|
|
29
|
+
[/- Components exported\/used that are NOT listed in `docs\/specs\/ui-components\.md`\n/, ''],
|
|
30
|
+
[/,? components not in ui-components\.md/, ''],
|
|
31
|
+
[/\.? ?Check components vs `ui-components\.md`/, ''],
|
|
32
|
+
],
|
|
33
|
+
'code-review-specialist.md': [
|
|
34
|
+
['(`backend-standards.mdc` / `frontend-standards.mdc`)', '(`backend-standards.mdc`)'],
|
|
35
|
+
['(`api-spec.yaml`, `ui-components.md`)', '(`api-spec.yaml`)'],
|
|
36
|
+
],
|
|
37
|
+
'qa-engineer.md': [
|
|
38
|
+
['(`api-spec.yaml`, `ui-components.md`)', '(`api-spec.yaml`)'],
|
|
39
|
+
[/- Frontend: `cd frontend && npm test`\n/, ''],
|
|
40
|
+
[/- \*\*Frontend\*\*: Write tests for error states[^\n]*\n/, ''],
|
|
41
|
+
['`backend-standards.mdc` / `frontend-standards.mdc`', '`backend-standards.mdc`'],
|
|
42
|
+
['`backend-standards.mdc` and/or `frontend-standards.mdc`', '`backend-standards.mdc`'],
|
|
43
|
+
[/ and\/or `(?:ai-specs\/specs\/)?frontend-standards\.mdc`/, ''],
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
frontend: {
|
|
47
|
+
'spec-creator.md': [
|
|
48
|
+
[/### Backend Specifications\n(?:- [^\n]*\n)+\n/, ''],
|
|
49
|
+
[/### For API Changes\n```yaml\n(?:[^\n]*\n)*?```\n\n/, ''],
|
|
50
|
+
['(`api-spec.yaml`, `ui-components.md`)', '(`ui-components.md`)'],
|
|
51
|
+
[/\(api-spec\.yaml, ui-components\.md\)/, '(ui-components.md)'],
|
|
52
|
+
],
|
|
53
|
+
'code-review-specialist.md': [
|
|
54
|
+
['(`backend-standards.mdc` / `frontend-standards.mdc`)', '(`frontend-standards.mdc`)'],
|
|
55
|
+
['(`api-spec.yaml`, `ui-components.md`)', '(`ui-components.md`)'],
|
|
56
|
+
],
|
|
57
|
+
'qa-engineer.md': [
|
|
58
|
+
['(`api-spec.yaml`, `ui-components.md`)', '(`ui-components.md`)'],
|
|
59
|
+
[/- Backend: `cd backend && npm test`\n/, ''],
|
|
60
|
+
[/- \*\*Backend\*\*: Write tests for error paths[^\n]*\n/, ''],
|
|
61
|
+
['`backend-standards.mdc` / `frontend-standards.mdc`', '`frontend-standards.mdc`'],
|
|
62
|
+
['`backend-standards.mdc` and/or `frontend-standards.mdc`', '`frontend-standards.mdc`'],
|
|
63
|
+
[/`(?:ai-specs\/specs\/)?backend-standards\.mdc` and\/or /, ''],
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pure function — apply single-stack adaptation rules to an agent file's content.
|
|
70
|
+
*
|
|
71
|
+
* Used by both the I/O wrapper below AND by upgrade-generator.js smart-diff
|
|
72
|
+
* (v0.16.10+) to compute what the "pristine adapted target" should be for a
|
|
73
|
+
* given (rawTemplate, filename, projectType) tuple. The smart-diff compares
|
|
74
|
+
* the user's current file against this value — if they match, the user hasn't
|
|
75
|
+
* customized and the upgrade can safely replace.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} content - Raw template content
|
|
78
|
+
* @param {string} filename - Agent file basename (e.g. 'spec-creator.md')
|
|
79
|
+
* @param {string} projectType - 'fullstack' | 'backend' | 'frontend'
|
|
80
|
+
* @returns {string} - Adapted content (or unchanged if no rules apply)
|
|
81
|
+
*/
|
|
82
|
+
function adaptAgentContentString(content, filename, projectType) {
|
|
83
|
+
if (projectType === 'fullstack') return content;
|
|
84
|
+
|
|
85
|
+
const rulesForType = AGENT_ADAPTATION_RULES[projectType];
|
|
86
|
+
if (!rulesForType) return content;
|
|
87
|
+
|
|
88
|
+
const rules = rulesForType[filename];
|
|
89
|
+
if (!rules) return content;
|
|
90
|
+
|
|
91
|
+
let result = content;
|
|
92
|
+
for (const [search, replace] of rules) {
|
|
93
|
+
if (search instanceof RegExp) {
|
|
94
|
+
result = result.replace(search, replace);
|
|
95
|
+
} else {
|
|
96
|
+
result = result.split(search).join(replace);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
5
102
|
/**
|
|
6
103
|
* Shared agent/skill content adaptation for single-stack projects.
|
|
7
104
|
* Used by both generator.js (new projects) and init-generator.js (--init).
|
|
8
105
|
*
|
|
106
|
+
* As of v0.16.10, delegates per-agent-file transformations to the pure
|
|
107
|
+
* adaptAgentContentString() function so upgrade-generator.js smart-diff can
|
|
108
|
+
* share the same rules. Skill and template file adaptations (SKILL.md,
|
|
109
|
+
* ticket-template.md, pr-template.md, AGENTS.md ui-ux-designer removal,
|
|
110
|
+
* base-standards.mdc cleanup) remain inline because they're not needed by
|
|
111
|
+
* smart-diff.
|
|
112
|
+
*
|
|
9
113
|
* @param {string} dest - Destination directory
|
|
10
114
|
* @param {object} config - Config with projectType and aiTools
|
|
11
115
|
* @param {function} replaceInFileFn - Function(filePath, replacements) to perform replacements
|
|
@@ -15,71 +119,20 @@ function adaptAgentContentForProjectType(dest, config, replaceInFileFn) {
|
|
|
15
119
|
if (config.aiTools !== 'gemini') toolDirs.push('.claude');
|
|
16
120
|
if (config.aiTools !== 'claude') toolDirs.push('.gemini');
|
|
17
121
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
replaceInFileFn(path.join(dest, dir, 'agents', 'spec-creator.md'), [
|
|
22
|
-
[/### Frontend Specifications\n(?:- [^\n]*\n)+\n/, ''],
|
|
23
|
-
[/### For UI Changes\n```markdown\n(?:[^\n]*\n)*?```\n\n/, ''],
|
|
24
|
-
['Data Model Changes, UI Changes, Edge Cases', 'Data Model Changes, Edge Cases'],
|
|
25
|
-
['(`api-spec.yaml`, `ui-components.md`)', '(`api-spec.yaml`)'],
|
|
26
|
-
]);
|
|
27
|
-
// production-code-validator: remove ui-components line from Spec Drift
|
|
28
|
-
replaceInFileFn(path.join(dest, dir, 'agents', 'production-code-validator.md'), [
|
|
29
|
-
[/- Components exported\/used that are NOT listed in `docs\/specs\/ui-components\.md`\n/, ''],
|
|
30
|
-
]);
|
|
31
|
-
// code-review-specialist: backend-only standards ref, remove ui-components
|
|
32
|
-
replaceInFileFn(path.join(dest, dir, 'agents', 'code-review-specialist.md'), [
|
|
33
|
-
['(`backend-standards.mdc` / `frontend-standards.mdc`)', '(`backend-standards.mdc`)'],
|
|
34
|
-
['(`api-spec.yaml`, `ui-components.md`)', '(`api-spec.yaml`)'],
|
|
35
|
-
]);
|
|
36
|
-
// qa-engineer: remove frontend refs, adapt standards refs
|
|
37
|
-
replaceInFileFn(path.join(dest, dir, 'agents', 'qa-engineer.md'), [
|
|
38
|
-
['(`api-spec.yaml`, `ui-components.md`)', '(`api-spec.yaml`)'],
|
|
39
|
-
[/- Frontend: `cd frontend && npm test`\n/, ''],
|
|
40
|
-
[/- \*\*Frontend\*\*: Write tests for error states[^\n]*\n/, ''],
|
|
41
|
-
['`backend-standards.mdc` / `frontend-standards.mdc`', '`backend-standards.mdc`'],
|
|
42
|
-
['`backend-standards.mdc` and/or `frontend-standards.mdc`', '`backend-standards.mdc`'],
|
|
43
|
-
[/ and\/or `(?:ai-specs\/specs\/)?frontend-standards\.mdc`/, ''],
|
|
44
|
-
]);
|
|
45
|
-
}
|
|
46
|
-
} else if (config.projectType === 'frontend') {
|
|
122
|
+
// --- Agent file adaptations (delegate to pure function) ---
|
|
123
|
+
const rulesForType = AGENT_ADAPTATION_RULES[config.projectType];
|
|
124
|
+
if (rulesForType) {
|
|
47
125
|
for (const dir of toolDirs) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
[/### For API Changes\n```yaml\n(?:[^\n]*\n)*?```\n\n/, ''],
|
|
52
|
-
['(`api-spec.yaml`, `ui-components.md`)', '(`ui-components.md`)'],
|
|
53
|
-
]);
|
|
54
|
-
// code-review-specialist: frontend-only standards ref
|
|
55
|
-
replaceInFileFn(path.join(dest, dir, 'agents', 'code-review-specialist.md'), [
|
|
56
|
-
['(`backend-standards.mdc` / `frontend-standards.mdc`)', '(`frontend-standards.mdc`)'],
|
|
57
|
-
['(`api-spec.yaml`, `ui-components.md`)', '(`ui-components.md`)'],
|
|
58
|
-
]);
|
|
59
|
-
// qa-engineer: remove backend refs, adapt standards refs
|
|
60
|
-
replaceInFileFn(path.join(dest, dir, 'agents', 'qa-engineer.md'), [
|
|
61
|
-
['(`api-spec.yaml`, `ui-components.md`)', '(`ui-components.md`)'],
|
|
62
|
-
[/- Backend: `cd backend && npm test`\n/, ''],
|
|
63
|
-
[/- \*\*Backend\*\*: Write tests for error paths[^\n]*\n/, ''],
|
|
64
|
-
['`backend-standards.mdc` / `frontend-standards.mdc`', '`frontend-standards.mdc`'],
|
|
65
|
-
['`backend-standards.mdc` and/or `frontend-standards.mdc`', '`frontend-standards.mdc`'],
|
|
66
|
-
[/`(?:ai-specs\/specs\/)?backend-standards\.mdc` and\/or /, ''],
|
|
67
|
-
]);
|
|
126
|
+
for (const [filename, rules] of Object.entries(rulesForType)) {
|
|
127
|
+
replaceInFileFn(path.join(dest, dir, 'agents', filename), rules);
|
|
128
|
+
}
|
|
68
129
|
}
|
|
69
130
|
}
|
|
70
131
|
|
|
71
|
-
// Skills and templates: remove frontend/backend-specific references
|
|
132
|
+
// --- Skills and templates: remove frontend/backend-specific references ---
|
|
133
|
+
// These stay inline — they're not agent files, so not needed by upgrade smart-diff.
|
|
72
134
|
if (config.projectType === 'backend') {
|
|
73
135
|
for (const dir of toolDirs) {
|
|
74
|
-
// Gemini agents have different text patterns for spec-creator
|
|
75
|
-
replaceInFileFn(path.join(dest, dir, 'agents', 'spec-creator.md'), [
|
|
76
|
-
[/\(api-spec\.yaml, ui-components\.md\)/, '(api-spec.yaml)'],
|
|
77
|
-
['Data Model Changes, UI Changes, Edge Cases', 'Data Model Changes, Edge Cases'],
|
|
78
|
-
]);
|
|
79
|
-
replaceInFileFn(path.join(dest, dir, 'agents', 'production-code-validator.md'), [
|
|
80
|
-
[/,? components not in ui-components\.md/, ''],
|
|
81
|
-
[/\.? ?Check components vs `ui-components\.md`/, ''],
|
|
82
|
-
]);
|
|
83
136
|
// SKILL.md: remove ui-components references and design review step
|
|
84
137
|
replaceInFileFn(path.join(dest, dir, 'skills', 'development-workflow', 'SKILL.md'), [
|
|
85
138
|
[/,? `ui-components\.md`\)/, ')'],
|
|
@@ -106,9 +159,6 @@ function adaptAgentContentForProjectType(dest, config, replaceInFileFn) {
|
|
|
106
159
|
]);
|
|
107
160
|
} else if (config.projectType === 'frontend') {
|
|
108
161
|
for (const dir of toolDirs) {
|
|
109
|
-
replaceInFileFn(path.join(dest, dir, 'agents', 'spec-creator.md'), [
|
|
110
|
-
[/\(api-spec\.yaml, ui-components\.md\)/, '(ui-components.md)'],
|
|
111
|
-
]);
|
|
112
162
|
replaceInFileFn(path.join(dest, dir, 'skills', 'development-workflow', 'SKILL.md'), [
|
|
113
163
|
[/`api-spec\.yaml`,? /, ''],
|
|
114
164
|
[/- API endpoints → `docs\/specs\/api-spec\.yaml` \(MANDATORY\)\n/, ''],
|
|
@@ -124,4 +174,8 @@ function adaptAgentContentForProjectType(dest, config, replaceInFileFn) {
|
|
|
124
174
|
}
|
|
125
175
|
}
|
|
126
176
|
|
|
127
|
-
module.exports = {
|
|
177
|
+
module.exports = {
|
|
178
|
+
adaptAgentContentForProjectType,
|
|
179
|
+
adaptAgentContentString,
|
|
180
|
+
AGENT_ADAPTATION_RULES,
|
|
181
|
+
};
|
package/lib/doctor.js
CHANGED
|
@@ -76,6 +76,12 @@ function runDoctor(cwd) {
|
|
|
76
76
|
// 13. Gemini TOML Commands Format
|
|
77
77
|
results.push(checkGeminiCommands(cwd, aiTools));
|
|
78
78
|
|
|
79
|
+
// 14. AGENTS.md Standards References (v0.16.10)
|
|
80
|
+
results.push(checkAgentsMdStandardsRefs(cwd));
|
|
81
|
+
|
|
82
|
+
// 15. .sdd-meta.json structural integrity (v0.17.0)
|
|
83
|
+
results.push(checkMetaJson(cwd, aiTools, projectType));
|
|
84
|
+
|
|
79
85
|
return results;
|
|
80
86
|
}
|
|
81
87
|
|
|
@@ -940,6 +946,221 @@ function checkGeminiCommands(cwd, aiTools) {
|
|
|
940
946
|
};
|
|
941
947
|
}
|
|
942
948
|
|
|
949
|
+
/**
|
|
950
|
+
* Check #14 (v0.16.10): detect adapter-failure artifacts in AGENTS.md.
|
|
951
|
+
*
|
|
952
|
+
* The specific broken state seen in foodXPlorer after the v0.16.9 upgrade
|
|
953
|
+
* was `Standards References` containing `"Backend patterns ()"` — empty
|
|
954
|
+
* parens left behind after the template substitution failed. This check
|
|
955
|
+
* looks for that pattern plus any unsubstituted placeholders of the shape
|
|
956
|
+
* `[Something]` that don't match a known-good markdown link.
|
|
957
|
+
*
|
|
958
|
+
* Severity: WARN — an empty-parens AGENTS.md doesn't break the project,
|
|
959
|
+
* it just leaves the agents without full stack context.
|
|
960
|
+
*/
|
|
961
|
+
function checkAgentsMdStandardsRefs(cwd) {
|
|
962
|
+
const agentsMdPath = path.join(cwd, 'AGENTS.md');
|
|
963
|
+
if (!fs.existsSync(agentsMdPath)) {
|
|
964
|
+
return {
|
|
965
|
+
status: WARN,
|
|
966
|
+
message: 'AGENTS.md: missing',
|
|
967
|
+
details: ['Run: npx create-sdd-project --upgrade to recreate'],
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const content = fs.readFileSync(agentsMdPath, 'utf8');
|
|
972
|
+
const issues = [];
|
|
973
|
+
|
|
974
|
+
// Detect adapter failure: "Backend patterns ()" or "Frontend patterns ()"
|
|
975
|
+
const emptyParensMatch = content.match(/(?:Backend|Frontend) patterns \(\s*\)/g);
|
|
976
|
+
if (emptyParensMatch) {
|
|
977
|
+
issues.push(
|
|
978
|
+
`Adapter failure: ${emptyParensMatch.join(', ')} (empty parens after template substitution)`
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Detect unsubstituted placeholders that look like "[Framework, runtime, version]".
|
|
983
|
+
// Template placeholders are distinctive: (a) they contain at least one
|
|
984
|
+
// comma-separated descriptor or the literal word "your", (b) they are NOT
|
|
985
|
+
// the target of a markdown link (no `(` or `:` immediately after the `]`).
|
|
986
|
+
//
|
|
987
|
+
// Gemini cross-model review (v0.16.10) caught the original broad regex
|
|
988
|
+
// /\[[A-Z][^\]]{5,60}\]/g catching legitimate user-added markdown links
|
|
989
|
+
// like `[Architecture Doc](./docs/arch.md)`. AGENTS.md is explicitly meant
|
|
990
|
+
// to be user-customized, so the doctor must not warn on every link.
|
|
991
|
+
//
|
|
992
|
+
// Match only placeholder-shaped strings that are followed by whitespace,
|
|
993
|
+
// end-of-line, or punctuation (not `(` for a link target or `:` for a
|
|
994
|
+
// footnote reference), AND contain either a comma or the word "your" or
|
|
995
|
+
// "example" to distinguish them from section headers.
|
|
996
|
+
const PLACEHOLDER_RE = /\[[A-Z][^\]]{3,60}[,\s](?:[^\]]{0,60})\](?!\(|:)/g;
|
|
997
|
+
const HINT_WORDS_RE = /\b(?:your|example|framework|runtime|version|name|path)\b/i;
|
|
998
|
+
const candidates = content.match(PLACEHOLDER_RE) || [];
|
|
999
|
+
const unsubstituted = candidates.filter((c) => HINT_WORDS_RE.test(c));
|
|
1000
|
+
if (unsubstituted.length > 0) {
|
|
1001
|
+
issues.push(`Unsubstituted placeholders: ${unsubstituted.slice(0, 3).join(', ')}`);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (issues.length > 0) {
|
|
1005
|
+
return {
|
|
1006
|
+
status: WARN,
|
|
1007
|
+
message: `AGENTS.md: ${issues.length} issue${issues.length > 1 ? 's' : ''}`,
|
|
1008
|
+
details: [...issues, 'Run: npx create-sdd-project --upgrade --force to re-adapt'],
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
return {
|
|
1013
|
+
status: PASS,
|
|
1014
|
+
message: 'AGENTS.md: standards references valid',
|
|
1015
|
+
details: [],
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Check #15 (v0.17.0): .sdd-meta.json structural integrity.
|
|
1021
|
+
*
|
|
1022
|
+
* v0.17.0 introduces content-addressable hashing via .sdd-meta.json to
|
|
1023
|
+
* track "the last time the tool wrote this file". The upgrade path uses
|
|
1024
|
+
* the hashes to answer "did the user edit since last tool-write" without
|
|
1025
|
+
* comparing against the new template's adapted output (which would drift
|
|
1026
|
+
* across versions — the Codex P1 from v0.16.10 cross-model review).
|
|
1027
|
+
*
|
|
1028
|
+
* This doctor check validates the METADATA STRUCTURE ONLY:
|
|
1029
|
+
* - Valid JSON
|
|
1030
|
+
* - schemaVersion ≤ current
|
|
1031
|
+
* - hashes is a sensible object shape
|
|
1032
|
+
* - Every hash value matches sha256:<64 hex>
|
|
1033
|
+
* - Every key that's NOT in the expected set is flagged as orphan
|
|
1034
|
+
*
|
|
1035
|
+
* It does NOT validate hashes against current on-disk content (Codex M3
|
|
1036
|
+
* from plan v1.0 review). Hash mismatches are the EXPECTED result of
|
|
1037
|
+
* legitimate user customization; reporting them here would generate
|
|
1038
|
+
* permanent noise and bury real integrity issues.
|
|
1039
|
+
*
|
|
1040
|
+
* Severity:
|
|
1041
|
+
* - File absent → PASS with informational message (pre-v0.17.0 project)
|
|
1042
|
+
* - Present and valid → PASS
|
|
1043
|
+
* - Parse/shape errors → WARN (not FAIL — upgrade still falls back safely)
|
|
1044
|
+
* - Orphan entries → WARN (non-fatal, upgrade prunes them next run)
|
|
1045
|
+
*/
|
|
1046
|
+
function checkMetaJson(cwd, aiTools, projectType) {
|
|
1047
|
+
const metaPath = path.join(cwd, '.sdd-meta.json');
|
|
1048
|
+
if (!fs.existsSync(metaPath)) {
|
|
1049
|
+
return {
|
|
1050
|
+
status: PASS,
|
|
1051
|
+
message: 'Provenance metadata: not present (pre-v0.17.0 project or fresh install)',
|
|
1052
|
+
details: [],
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
let raw;
|
|
1057
|
+
try {
|
|
1058
|
+
raw = fs.readFileSync(metaPath, 'utf8');
|
|
1059
|
+
} catch (e) {
|
|
1060
|
+
return {
|
|
1061
|
+
status: WARN,
|
|
1062
|
+
message: '.sdd-meta.json: unreadable',
|
|
1063
|
+
details: [e.code || e.message],
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
let parsed;
|
|
1068
|
+
try {
|
|
1069
|
+
parsed = JSON.parse(raw);
|
|
1070
|
+
} catch (e) {
|
|
1071
|
+
return {
|
|
1072
|
+
status: WARN,
|
|
1073
|
+
message: '.sdd-meta.json: invalid JSON',
|
|
1074
|
+
details: [e.message, 'Next upgrade will regenerate it via the fallback path.'],
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
1079
|
+
return {
|
|
1080
|
+
status: WARN,
|
|
1081
|
+
message: '.sdd-meta.json: root is not an object',
|
|
1082
|
+
details: ['Next upgrade will regenerate it.'],
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Validate schemaVersion. Absent = v1 (forward-compat). Newer than
|
|
1087
|
+
// supported = still WARN (fallback path handles it).
|
|
1088
|
+
const {
|
|
1089
|
+
CURRENT_SCHEMA_VERSION,
|
|
1090
|
+
expectedSmartDiffTrackedPaths,
|
|
1091
|
+
} = require('./meta');
|
|
1092
|
+
const schemaVersion = parsed.schemaVersion ?? 1;
|
|
1093
|
+
if (typeof schemaVersion !== 'number' || schemaVersion < 1) {
|
|
1094
|
+
return {
|
|
1095
|
+
status: WARN,
|
|
1096
|
+
message: `.sdd-meta.json: invalid schemaVersion ${schemaVersion}`,
|
|
1097
|
+
details: [],
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
if (schemaVersion > CURRENT_SCHEMA_VERSION) {
|
|
1101
|
+
return {
|
|
1102
|
+
status: WARN,
|
|
1103
|
+
message: `.sdd-meta.json: schemaVersion ${schemaVersion} newer than supported ${CURRENT_SCHEMA_VERSION}`,
|
|
1104
|
+
details: ['Upgrade the sdd-devflow CLI to the latest version.'],
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const hashes = parsed.hashes;
|
|
1109
|
+
if (typeof hashes !== 'object' || hashes === null || Array.isArray(hashes)) {
|
|
1110
|
+
return {
|
|
1111
|
+
status: WARN,
|
|
1112
|
+
message: '.sdd-meta.json: hashes field is not an object',
|
|
1113
|
+
details: ['Next upgrade will regenerate it.'],
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Validate each entry's shape.
|
|
1118
|
+
const HASH_RE = /^sha256:[0-9a-f]{64}$/;
|
|
1119
|
+
const issues = [];
|
|
1120
|
+
for (const [k, v] of Object.entries(hashes)) {
|
|
1121
|
+
if (typeof k !== 'string') {
|
|
1122
|
+
issues.push(`invalid key: ${typeof k}`);
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
// Reject absolute paths and `..` traversal.
|
|
1126
|
+
if (k.startsWith('/') || k.includes('..')) {
|
|
1127
|
+
issues.push(`suspicious key: ${k}`);
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
if (typeof v !== 'string' || !HASH_RE.test(v)) {
|
|
1131
|
+
issues.push(`invalid hash for ${k}`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (issues.length > 0) {
|
|
1136
|
+
return {
|
|
1137
|
+
status: WARN,
|
|
1138
|
+
message: `.sdd-meta.json: ${issues.length} shape issue${issues.length > 1 ? 's' : ''}`,
|
|
1139
|
+
details: [...issues.slice(0, 5), 'Next upgrade will regenerate affected entries.'],
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Detect orphan entries (keys not expected for the current tool/type).
|
|
1144
|
+
const expected = expectedSmartDiffTrackedPaths(aiTools, projectType);
|
|
1145
|
+
const orphans = Object.keys(hashes).filter((k) => !expected.has(k));
|
|
1146
|
+
if (orphans.length > 0) {
|
|
1147
|
+
return {
|
|
1148
|
+
status: WARN,
|
|
1149
|
+
message: `.sdd-meta.json: ${orphans.length} orphan entr${orphans.length > 1 ? 'ies' : 'y'}`,
|
|
1150
|
+
details: [
|
|
1151
|
+
...orphans.slice(0, 5).map((o) => `Orphan: ${o}`),
|
|
1152
|
+
'Non-fatal — next upgrade will prune these automatically.',
|
|
1153
|
+
],
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
return {
|
|
1158
|
+
status: PASS,
|
|
1159
|
+
message: `Provenance metadata: valid (${Object.keys(hashes).length} tracked files)`,
|
|
1160
|
+
details: [],
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
943
1164
|
module.exports = {
|
|
944
1165
|
runDoctor,
|
|
945
1166
|
printResults,
|
package/lib/generator.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
BACKEND_AGENTS,
|
|
11
11
|
} = require('./config');
|
|
12
12
|
const { adaptAgentContentForProjectType } = require('./adapt-agents');
|
|
13
|
+
const { writeMeta, computeInstallHashes } = require('./meta');
|
|
13
14
|
|
|
14
15
|
function generate(config) {
|
|
15
16
|
const templateDir = path.join(__dirname, '..', 'template');
|
|
@@ -95,6 +96,13 @@ function generate(config) {
|
|
|
95
96
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
96
97
|
fs.writeFileSync(path.join(dest, '.sdd-version'), pkg.version + '\n', 'utf8');
|
|
97
98
|
|
|
99
|
+
// v0.17.0: write provenance hashes. Captures whatever generator.js's
|
|
100
|
+
// pipeline produced (lighter than init-generator — adaptAgentsForStack
|
|
101
|
+
// only, no stack-adaptations module). First upgrade re-adapts via the
|
|
102
|
+
// scanner-driven pipeline; the hash answers "did the user edit since
|
|
103
|
+
// install" precisely regardless of which pipeline wrote the content.
|
|
104
|
+
writeMeta(dest, computeInstallHashes(dest, config.aiTools, config.projectType));
|
|
105
|
+
|
|
98
106
|
// Show notes
|
|
99
107
|
const notes = collectNotes(config);
|
|
100
108
|
if (notes.length > 0) {
|