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 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 = {
@@ -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
- if (config.projectType === 'backend') {
19
- for (const dir of toolDirs) {
20
- // spec-creator: remove Frontend Specifications section, UI output format, ui-components refs
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
- // spec-creator: remove Backend Specifications section, api-spec refs
49
- replaceInFileFn(path.join(dest, dir, 'agents', 'spec-creator.md'), [
50
- [/### Backend Specifications\n(?:- [^\n]*\n)+\n/, ''],
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 = { adaptAgentContentForProjectType };
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) {