create-sdd-project 0.16.10 → 0.17.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.
@@ -65,6 +65,99 @@ const AGENT_ADAPTATION_RULES = {
65
65
  },
66
66
  };
67
67
 
68
+ /**
69
+ * v0.17.1: project-type-specific pruning rules for workflow-core files
70
+ * (SKILL.md + ticket-template.md). Keys are POSIX suffixes relative to
71
+ * the tool dir (e.g. `skills/development-workflow/SKILL.md`) so the same
72
+ * rules apply to both `.claude/` and `.gemini/` trees.
73
+ *
74
+ * These rules are the pure/in-memory equivalent of the inline block in
75
+ * `adaptAgentContentForProjectType`. Exposed here so upgrade-generator.js
76
+ * can build an accurate "what init would have produced" comparison target
77
+ * for smart-diff fallback paths — previously this was masked by
78
+ * unconditional `filesToAdapt.add` calls (pre-v0.17.1) that re-applied
79
+ * stack rules to restored user content, violating Codex M1 (Gemini round-3
80
+ * finding 1). Source of truth now lives here; disk-writing code below
81
+ * calls these same tables.
82
+ */
83
+ const WORKFLOW_CORE_PROJECT_TYPE_RULES = {
84
+ backend: {
85
+ 'skills/development-workflow/SKILL.md': [
86
+ [/,? `ui-components\.md`\)/, ')'],
87
+ [/- UI components → `docs\/specs\/ui-components\.md` \(MANDATORY\)\n/, ''],
88
+ [/\d+\. \*\*Design Review \(optional\):\*\*[^\n]*\n/, ''],
89
+ ],
90
+ 'skills/development-workflow/references/ticket-template.md': [
91
+ [/### UI Changes \(if applicable\)\n\n\[Components to add\/modify\. Reference `docs\/specs\/ui-components\.md`\.\]\n\n/, ''],
92
+ [' / `ui-components.md`', ''],
93
+ ],
94
+ },
95
+ frontend: {
96
+ 'skills/development-workflow/SKILL.md': [
97
+ [/`api-spec\.yaml`,? /, ''],
98
+ [/- API endpoints → `docs\/specs\/api-spec\.yaml` \(MANDATORY\)\n/, ''],
99
+ ],
100
+ 'skills/development-workflow/references/ticket-template.md': [
101
+ [/### API Changes \(if applicable\)\n\n\[Endpoints to add\/modify\. Reference[^\]]*\]\n\n/, ''],
102
+ ['`api-spec.yaml` / ', ''],
103
+ ],
104
+ },
105
+ };
106
+
107
+ const BASE_STANDARDS_PROJECT_TYPE_RULES = {
108
+ backend: [
109
+ [/\| `ui-ux-designer` \|[^\n]*\n/, ''],
110
+ ],
111
+ // frontend: no extra rules (base-standards template has no frontend-only refs to strip)
112
+ };
113
+
114
+ function applyProjectTypeRules(content, rules) {
115
+ let result = content;
116
+ for (const [search, replace] of rules) {
117
+ if (search instanceof RegExp) {
118
+ result = result.replace(search, replace);
119
+ } else {
120
+ result = result.split(search).join(replace);
121
+ }
122
+ }
123
+ return result;
124
+ }
125
+
126
+ /**
127
+ * v0.17.1 pure helper — apply project-type rules to a workflow-core file
128
+ * content. Returns content unchanged if projectType is fullstack or if
129
+ * posixPath doesn't match a known workflow-core file.
130
+ *
131
+ * @param {string} content - Raw or stack-adapted content
132
+ * @param {string} posixPath - Full POSIX path (e.g. '.claude/skills/development-workflow/SKILL.md')
133
+ * @param {string} projectType - 'fullstack' | 'backend' | 'frontend'
134
+ * @returns {string}
135
+ */
136
+ function adaptWorkflowCoreContentForProjectType(content, posixPath, projectType) {
137
+ if (projectType === 'fullstack') return content;
138
+ const rulesForType = WORKFLOW_CORE_PROJECT_TYPE_RULES[projectType];
139
+ if (!rulesForType) return content;
140
+
141
+ // Strip the tool prefix (.claude/ or .gemini/) to match the rule key.
142
+ const match = posixPath.match(/^\.(?:claude|gemini)\/(.+)$/);
143
+ if (!match) return content;
144
+ const rules = rulesForType[match[1]];
145
+ if (!rules) return content;
146
+
147
+ return applyProjectTypeRules(content, rules);
148
+ }
149
+
150
+ /**
151
+ * v0.17.1 pure helper — apply project-type rules to base-standards.mdc
152
+ * content. Called AFTER `adaptBaseStandards` to produce the full init
153
+ * equivalent for smart-diff comparison.
154
+ */
155
+ function adaptBaseStandardsContentForProjectType(content, projectType) {
156
+ const rules = BASE_STANDARDS_PROJECT_TYPE_RULES[projectType];
157
+ if (!rules) return content;
158
+ return applyProjectTypeRules(content, rules);
159
+ }
160
+
68
161
  /**
69
162
  * Pure function — apply single-stack adaptation rules to an agent file's content.
70
163
  *
@@ -130,43 +223,38 @@ function adaptAgentContentForProjectType(dest, config, replaceInFileFn) {
130
223
  }
131
224
 
132
225
  // --- Skills and templates: remove frontend/backend-specific references ---
133
- // These stay inline they're not agent files, so not needed by upgrade smart-diff.
226
+ // v0.17.1: SKILL.md + ticket-template.md rules now come from the
227
+ // WORKFLOW_CORE_PROJECT_TYPE_RULES table above so upgrade-generator.js
228
+ // can apply the same rules in-memory (smart-diff fallback comparison).
229
+ // pr-template.md + AGENTS.md + base-standards.mdc remain inline because
230
+ // they're not workflow-core files (pr-template is v0.17.2 scope).
231
+ const wfRules = WORKFLOW_CORE_PROJECT_TYPE_RULES[config.projectType];
232
+ if (wfRules) {
233
+ for (const dir of toolDirs) {
234
+ for (const [suffix, rules] of Object.entries(wfRules)) {
235
+ replaceInFileFn(path.join(dest, dir, ...suffix.split('/')), rules);
236
+ }
237
+ }
238
+ }
239
+
134
240
  if (config.projectType === 'backend') {
241
+ // AGENTS.md: remove ui-ux-designer from hook description
242
+ replaceInFileFn(path.join(dest, 'AGENTS.md'), [
243
+ [', `ui-ux-designer`', ''],
244
+ ]);
135
245
  for (const dir of toolDirs) {
136
- // SKILL.md: remove ui-components references and design review step
137
- replaceInFileFn(path.join(dest, dir, 'skills', 'development-workflow', 'SKILL.md'), [
138
- [/,? `ui-components\.md`\)/, ')'],
139
- [/- UI components → `docs\/specs\/ui-components\.md` \(MANDATORY\)\n/, ''],
140
- [/\d+\. \*\*Design Review \(optional\):\*\*[^\n]*\n/, ''],
141
- ]);
142
- // AGENTS.md: remove ui-ux-designer from hook description
143
- replaceInFileFn(path.join(dest, 'AGENTS.md'), [
144
- [', `ui-ux-designer`', ''],
145
- ]);
146
- // ticket-template: remove UI Changes section, ui-components from checklists
147
- replaceInFileFn(path.join(dest, dir, 'skills', 'development-workflow', 'references', 'ticket-template.md'), [
148
- [/### UI Changes \(if applicable\)\n\n\[Components to add\/modify\. Reference `docs\/specs\/ui-components\.md`\.\]\n\n/, ''],
149
- [' / `ui-components.md`', ''],
150
- ]);
151
- // pr-template: remove ui-components from checklist
246
+ // pr-template: remove ui-components from checklist (v0.17.2 scope)
152
247
  replaceInFileFn(path.join(dest, dir, 'skills', 'development-workflow', 'references', 'pr-template.md'), [
153
248
  [' / ui-components.md', ''],
154
249
  ]);
155
250
  }
156
- // Shared files (outside tool dirs): remove ui-ux-designer and design-guidelines refs
157
- replaceInFileFn(path.join(dest, 'ai-specs', 'specs', 'base-standards.mdc'), [
158
- [/\| `ui-ux-designer` \|[^\n]*\n/, ''],
159
- ]);
251
+ // base-standards.mdc: remove ui-ux-designer table row (shared table above)
252
+ replaceInFileFn(
253
+ path.join(dest, 'ai-specs', 'specs', 'base-standards.mdc'),
254
+ BASE_STANDARDS_PROJECT_TYPE_RULES.backend
255
+ );
160
256
  } else if (config.projectType === 'frontend') {
161
257
  for (const dir of toolDirs) {
162
- replaceInFileFn(path.join(dest, dir, 'skills', 'development-workflow', 'SKILL.md'), [
163
- [/`api-spec\.yaml`,? /, ''],
164
- [/- API endpoints → `docs\/specs\/api-spec\.yaml` \(MANDATORY\)\n/, ''],
165
- ]);
166
- replaceInFileFn(path.join(dest, dir, 'skills', 'development-workflow', 'references', 'ticket-template.md'), [
167
- [/### API Changes \(if applicable\)\n\n\[Endpoints to add\/modify\. Reference[^\]]*\]\n\n/, ''],
168
- ['`api-spec.yaml` / ', ''],
169
- ]);
170
258
  replaceInFileFn(path.join(dest, dir, 'skills', 'development-workflow', 'references', 'pr-template.md'), [
171
259
  ['api-spec.yaml / ', ''],
172
260
  ]);
@@ -177,5 +265,9 @@ function adaptAgentContentForProjectType(dest, config, replaceInFileFn) {
177
265
  module.exports = {
178
266
  adaptAgentContentForProjectType,
179
267
  adaptAgentContentString,
268
+ adaptWorkflowCoreContentForProjectType,
269
+ adaptBaseStandardsContentForProjectType,
180
270
  AGENT_ADAPTATION_RULES,
271
+ WORKFLOW_CORE_PROJECT_TYPE_RULES,
272
+ BASE_STANDARDS_PROJECT_TYPE_RULES,
181
273
  };
@@ -13,11 +13,17 @@ const {
13
13
  adaptFrontendStandards,
14
14
  } = require('./init-generator');
15
15
  const {
16
- isStandardModified,
17
16
  getPackageVersion,
18
17
  } = require('./upgrade-generator');
18
+ const { normalizedContentEquals } = require('./meta');
19
19
  const { formatScanSummary } = require('./init-wizard');
20
20
 
21
+ // v0.17.1: isStandardModified was removed. Replace callers with inverted
22
+ // normalizedContentEquals — "modified" means "not equal after normalization".
23
+ function isStandardModified(existing, fresh) {
24
+ return !normalizedContentEquals(existing, fresh);
25
+ }
26
+
21
27
  const templateDir = path.join(__dirname, '..', 'template');
22
28
 
23
29
  /**
package/lib/doctor.js CHANGED
@@ -79,6 +79,9 @@ function runDoctor(cwd) {
79
79
  // 14. AGENTS.md Standards References (v0.16.10)
80
80
  results.push(checkAgentsMdStandardsRefs(cwd));
81
81
 
82
+ // 15. .sdd-meta.json structural integrity (v0.17.0)
83
+ results.push(checkMetaJson(cwd, aiTools, projectType));
84
+
82
85
  return results;
83
86
  }
84
87
 
@@ -976,6 +979,24 @@ function checkAgentsMdStandardsRefs(cwd) {
976
979
  );
977
980
  }
978
981
 
982
+ // v0.17.1 observability (Gemini Q10): warn on sparse Backend/Frontend patterns
983
+ // — exactly 1 entry — suggesting scanner detection missed framework or ORM.
984
+ // Permissive: non-failing, informational. Two+ entries are assumed OK because
985
+ // projects legitimately vary (ORM-only backends, component-less frontends).
986
+ const sparseRe = /(Backend|Frontend) patterns \(([^)]+)\)/g;
987
+ let sparseMatch;
988
+ while ((sparseMatch = sparseRe.exec(content)) !== null) {
989
+ const rawEntries = sparseMatch[2]
990
+ .split(',')
991
+ .map((s) => s.trim())
992
+ .filter((s) => s.length > 0);
993
+ if (rawEntries.length === 1) {
994
+ issues.push(
995
+ `${sparseMatch[1]} patterns has only 1 entry (${rawEntries[0]}) — scanner detection may be incomplete; run \`npx create-sdd-project --upgrade\` after installing your stack deps to re-detect`
996
+ );
997
+ }
998
+ }
999
+
979
1000
  // Detect unsubstituted placeholders that look like "[Framework, runtime, version]".
980
1001
  // Template placeholders are distinctive: (a) they contain at least one
981
1002
  // comma-separated descriptor or the literal word "your", (b) they are NOT
@@ -1013,6 +1034,151 @@ function checkAgentsMdStandardsRefs(cwd) {
1013
1034
  };
1014
1035
  }
1015
1036
 
1037
+ /**
1038
+ * Check #15 (v0.17.0): .sdd-meta.json structural integrity.
1039
+ *
1040
+ * v0.17.0 introduces content-addressable hashing via .sdd-meta.json to
1041
+ * track "the last time the tool wrote this file". The upgrade path uses
1042
+ * the hashes to answer "did the user edit since last tool-write" without
1043
+ * comparing against the new template's adapted output (which would drift
1044
+ * across versions — the Codex P1 from v0.16.10 cross-model review).
1045
+ *
1046
+ * This doctor check validates the METADATA STRUCTURE ONLY:
1047
+ * - Valid JSON
1048
+ * - schemaVersion ≤ current
1049
+ * - hashes is a sensible object shape
1050
+ * - Every hash value matches sha256:<64 hex>
1051
+ * - Every key that's NOT in the expected set is flagged as orphan
1052
+ *
1053
+ * It does NOT validate hashes against current on-disk content (Codex M3
1054
+ * from plan v1.0 review). Hash mismatches are the EXPECTED result of
1055
+ * legitimate user customization; reporting them here would generate
1056
+ * permanent noise and bury real integrity issues.
1057
+ *
1058
+ * Severity:
1059
+ * - File absent → PASS with informational message (pre-v0.17.0 project)
1060
+ * - Present and valid → PASS
1061
+ * - Parse/shape errors → WARN (not FAIL — upgrade still falls back safely)
1062
+ * - Orphan entries → WARN (non-fatal, upgrade prunes them next run)
1063
+ */
1064
+ function checkMetaJson(cwd, aiTools, projectType) {
1065
+ const metaPath = path.join(cwd, '.sdd-meta.json');
1066
+ if (!fs.existsSync(metaPath)) {
1067
+ return {
1068
+ status: PASS,
1069
+ message: 'Provenance metadata: not present (pre-v0.17.0 project or fresh install)',
1070
+ details: [],
1071
+ };
1072
+ }
1073
+
1074
+ let raw;
1075
+ try {
1076
+ raw = fs.readFileSync(metaPath, 'utf8');
1077
+ } catch (e) {
1078
+ return {
1079
+ status: WARN,
1080
+ message: '.sdd-meta.json: unreadable',
1081
+ details: [e.code || e.message],
1082
+ };
1083
+ }
1084
+
1085
+ let parsed;
1086
+ try {
1087
+ parsed = JSON.parse(raw);
1088
+ } catch (e) {
1089
+ return {
1090
+ status: WARN,
1091
+ message: '.sdd-meta.json: invalid JSON',
1092
+ details: [e.message, 'Next upgrade will regenerate it via the fallback path.'],
1093
+ };
1094
+ }
1095
+
1096
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
1097
+ return {
1098
+ status: WARN,
1099
+ message: '.sdd-meta.json: root is not an object',
1100
+ details: ['Next upgrade will regenerate it.'],
1101
+ };
1102
+ }
1103
+
1104
+ // Validate schemaVersion. Absent = v1 (forward-compat). Newer than
1105
+ // supported = still WARN (fallback path handles it).
1106
+ const {
1107
+ CURRENT_SCHEMA_VERSION,
1108
+ expectedSmartDiffTrackedPaths,
1109
+ } = require('./meta');
1110
+ const schemaVersion = parsed.schemaVersion ?? 1;
1111
+ if (typeof schemaVersion !== 'number' || schemaVersion < 1) {
1112
+ return {
1113
+ status: WARN,
1114
+ message: `.sdd-meta.json: invalid schemaVersion ${schemaVersion}`,
1115
+ details: [],
1116
+ };
1117
+ }
1118
+ if (schemaVersion > CURRENT_SCHEMA_VERSION) {
1119
+ return {
1120
+ status: WARN,
1121
+ message: `.sdd-meta.json: schemaVersion ${schemaVersion} newer than supported ${CURRENT_SCHEMA_VERSION}`,
1122
+ details: ['Upgrade the sdd-devflow CLI to the latest version.'],
1123
+ };
1124
+ }
1125
+
1126
+ const hashes = parsed.hashes;
1127
+ if (typeof hashes !== 'object' || hashes === null || Array.isArray(hashes)) {
1128
+ return {
1129
+ status: WARN,
1130
+ message: '.sdd-meta.json: hashes field is not an object',
1131
+ details: ['Next upgrade will regenerate it.'],
1132
+ };
1133
+ }
1134
+
1135
+ // Validate each entry's shape.
1136
+ const HASH_RE = /^sha256:[0-9a-f]{64}$/;
1137
+ const issues = [];
1138
+ for (const [k, v] of Object.entries(hashes)) {
1139
+ if (typeof k !== 'string') {
1140
+ issues.push(`invalid key: ${typeof k}`);
1141
+ continue;
1142
+ }
1143
+ // Reject absolute paths and `..` traversal.
1144
+ if (k.startsWith('/') || k.includes('..')) {
1145
+ issues.push(`suspicious key: ${k}`);
1146
+ continue;
1147
+ }
1148
+ if (typeof v !== 'string' || !HASH_RE.test(v)) {
1149
+ issues.push(`invalid hash for ${k}`);
1150
+ }
1151
+ }
1152
+
1153
+ if (issues.length > 0) {
1154
+ return {
1155
+ status: WARN,
1156
+ message: `.sdd-meta.json: ${issues.length} shape issue${issues.length > 1 ? 's' : ''}`,
1157
+ details: [...issues.slice(0, 5), 'Next upgrade will regenerate affected entries.'],
1158
+ };
1159
+ }
1160
+
1161
+ // Detect orphan entries (keys not expected for the current tool/type).
1162
+ const expected = expectedSmartDiffTrackedPaths(aiTools, projectType);
1163
+ const orphans = Object.keys(hashes).filter((k) => !expected.has(k));
1164
+ if (orphans.length > 0) {
1165
+ return {
1166
+ status: WARN,
1167
+ message: `.sdd-meta.json: ${orphans.length} orphan entr${orphans.length > 1 ? 'ies' : 'y'}`,
1168
+ details: [
1169
+ ...orphans.slice(0, 5).map((o) => `Orphan: ${o}`),
1170
+ 'Non-fatal — next upgrade will prune these automatically.',
1171
+ ],
1172
+ };
1173
+ }
1174
+
1175
+ return {
1176
+ status: PASS,
1177
+ message: `Provenance metadata: valid (${Object.keys(hashes).length} tracked files)`,
1178
+ details: [],
1179
+ };
1180
+ }
1181
+
1016
1182
  module.exports = {
1017
1183
  runDoctor,
1018
1184
  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) {