gsdd-cli 0.19.0 → 0.19.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.
@@ -1,6 +1,11 @@
1
1
  const EVIDENCE_KINDS = Object.freeze(['code', 'test', 'runtime', 'delivery', 'human']);
2
2
  const DELIVERY_POSTURES = Object.freeze(['repo_only', 'delivery_sensitive']);
3
3
  const CLOSURE_SURFACES = Object.freeze(['verify', 'audit-milestone', 'complete-milestone']);
4
+ const RELEASE_CLAIM_POSTURES = Object.freeze([
5
+ 'repo_closeout',
6
+ 'runtime_validated_closeout',
7
+ 'delivery_supported_closeout',
8
+ ]);
4
9
 
5
10
  const LEGACY_EVIDENCE_ALIASES = Object.freeze({
6
11
  code: 'code',
@@ -22,8 +27,8 @@ const EVIDENCE_MATRIX = Object.freeze({
22
27
  blockedSoloKinds: Object.freeze(['human', 'delivery']),
23
28
  }),
24
29
  delivery_sensitive: Object.freeze({
25
- requiredKinds: Object.freeze(['code', 'runtime']),
26
- recommendedKinds: Object.freeze(['test', 'delivery', 'human']),
30
+ requiredKinds: Object.freeze(['code', 'runtime', 'delivery']),
31
+ recommendedKinds: Object.freeze(['test', 'human']),
27
32
  blockedSoloKinds: Object.freeze(['code', 'human']),
28
33
  }),
29
34
  }),
@@ -53,7 +58,45 @@ const EVIDENCE_MATRIX = Object.freeze({
53
58
  }),
54
59
  });
55
60
 
56
- export { CLOSURE_SURFACES, DELIVERY_POSTURES, EVIDENCE_KINDS };
61
+ const CONTRADICTION_CATEGORIES = Object.freeze([
62
+ 'evidence',
63
+ 'public_surface',
64
+ 'runtime',
65
+ 'delivery',
66
+ 'planning_drift',
67
+ 'generated_surface',
68
+ ]);
69
+
70
+ const CONTRADICTION_STATUSES = Object.freeze(['passed', 'failed', 'not_applicable']);
71
+
72
+ const RELEASE_CLAIM_MATRIX = Object.freeze({
73
+ repo_closeout: Object.freeze({
74
+ deliveryPosture: 'repo_only',
75
+ requiredClaimKinds: Object.freeze([]),
76
+ allowedClaim: 'Repo-local milestone or phase closeout is supported by planning and repository artifacts only.',
77
+ invalidClaim: 'Do not imply runtime validation, delivery, publication, or public support from repo-local closeout alone.',
78
+ }),
79
+ runtime_validated_closeout: Object.freeze({
80
+ deliveryPosture: 'repo_only',
81
+ requiredClaimKinds: Object.freeze(['runtime']),
82
+ allowedClaim: 'Runtime behavior or a runtime surface was directly executed and observed for the named runtime or surface.',
83
+ invalidClaim: 'Do not generalize validation from one runtime or generated surface to another.',
84
+ }),
85
+ delivery_supported_closeout: Object.freeze({
86
+ deliveryPosture: 'delivery_sensitive',
87
+ requiredClaimKinds: Object.freeze([]),
88
+ allowedClaim: 'Externally consumed release, support, install, or delivery claims are supported by the delivery-sensitive evidence bar.',
89
+ invalidClaim: 'Do not imply merge, package, tag, GitHub Release, publication, generated-surface freshness, or public support without matching delivery evidence.',
90
+ }),
91
+ });
92
+
93
+ const CONTRADICTION_BLOCKERS_BY_POSTURE = Object.freeze({
94
+ repo_closeout: Object.freeze(['evidence', 'public_surface', 'planning_drift']),
95
+ runtime_validated_closeout: Object.freeze(['evidence', 'runtime', 'generated_surface', 'planning_drift']),
96
+ delivery_supported_closeout: CONTRADICTION_CATEGORIES,
97
+ });
98
+
99
+ export { CLOSURE_SURFACES, DELIVERY_POSTURES, EVIDENCE_KINDS, RELEASE_CLAIM_POSTURES };
57
100
 
58
101
  export function normalizeEvidenceKind(kind) {
59
102
  if (!kind) {
@@ -78,6 +121,11 @@ export function isClosureSurface(surface) {
78
121
  return CLOSURE_SURFACES.includes(surface);
79
122
  }
80
123
 
124
+ export function normalizeReleaseClaimPosture(posture) {
125
+ if (!posture) return 'repo_closeout';
126
+ return RELEASE_CLAIM_POSTURES.includes(posture) ? posture : null;
127
+ }
128
+
81
129
  export function getEvidenceContract(surface, deliveryPosture) {
82
130
  const matrix = EVIDENCE_MATRIX[surface];
83
131
  if (!matrix) {
@@ -108,5 +156,170 @@ export function describeEvidenceSurface(surface) {
108
156
  surface,
109
157
  supportedKinds: [...EVIDENCE_KINDS],
110
158
  deliveryPostures: DELIVERY_POSTURES.map((deliveryPosture) => getEvidenceContract(surface, deliveryPosture)),
159
+ releaseClaimPostures: RELEASE_CLAIM_POSTURES.map((releaseClaimPosture) => getReleaseClaimContract(surface, releaseClaimPosture)),
160
+ };
161
+ }
162
+
163
+ function uniqueKinds(kinds) {
164
+ return [...new Set(kinds)];
165
+ }
166
+
167
+ function getDowngradePosture(observedKinds) {
168
+ if (observedKinds.includes('runtime')) {
169
+ return 'runtime_validated_closeout';
170
+ }
171
+ return 'repo_closeout';
172
+ }
173
+
174
+ export function getReleaseClaimContract(surface, releaseClaimPosture = 'repo_closeout') {
175
+ const posture = normalizeReleaseClaimPosture(releaseClaimPosture);
176
+ if (!posture) {
177
+ throw new Error(`Unsupported release claim posture: ${releaseClaimPosture}`);
178
+ }
179
+ const claim = RELEASE_CLAIM_MATRIX[posture];
180
+ const evidence = getEvidenceContract(surface, claim.deliveryPosture);
181
+ const requiredKinds = uniqueKinds([...evidence.requiredKinds, ...claim.requiredClaimKinds]);
182
+
183
+ return {
184
+ surface,
185
+ releaseClaimPosture: posture,
186
+ deliveryPosture: claim.deliveryPosture,
187
+ supportedKinds: [...EVIDENCE_KINDS],
188
+ requiredKinds,
189
+ requiredClaimKinds: [...claim.requiredClaimKinds],
190
+ allowedClaim: claim.allowedClaim,
191
+ invalidClaim: claim.invalidClaim,
192
+ waiverRule: 'Waivers may only narrow the release claim posture or defer an unsupported claim; they never satisfy missing required evidence for the stronger claim.',
193
+ deferralRule: 'Deferrals must name the unsupported claim, missing evidence kinds, and later workflow or milestone candidate when known.',
194
+ contradictionCategories: [...CONTRADICTION_CATEGORIES],
195
+ };
196
+ }
197
+
198
+ export function evaluateReleaseClaimPosture({
199
+ surface,
200
+ releaseClaimPosture = 'repo_closeout',
201
+ observedKinds = [],
202
+ waivedKinds = [],
203
+ } = {}) {
204
+ const contract = getReleaseClaimContract(surface, releaseClaimPosture);
205
+ const observed = normalizeEvidenceKinds(observedKinds);
206
+ const waived = normalizeEvidenceKinds(waivedKinds);
207
+ const missingKinds = contract.requiredKinds.filter((kind) => !observed.includes(kind));
208
+ const invalidWaivers = waived.filter((kind) => missingKinds.includes(kind));
209
+ const hasUnsupportedStrongClaim = contract.releaseClaimPosture !== 'repo_closeout' && missingKinds.length > 0;
210
+
211
+ return {
212
+ surface: contract.surface,
213
+ releaseClaimPosture: contract.releaseClaimPosture,
214
+ deliveryPosture: contract.deliveryPosture,
215
+ requiredKinds: [...contract.requiredKinds],
216
+ observedKinds: observed,
217
+ missingKinds,
218
+ invalidWaivers,
219
+ status: missingKinds.length === 0 && invalidWaivers.length === 0 ? 'supported' : 'unsupported',
220
+ disposition: hasUnsupportedStrongClaim ? 'downgrade_or_defer' : missingKinds.length > 0 ? 'block_or_defer' : 'proceed',
221
+ downgradeTo: hasUnsupportedStrongClaim ? getDowngradePosture(observed) : null,
222
+ deferredClaims: hasUnsupportedStrongClaim
223
+ ? [{ claim: contract.releaseClaimPosture, missingKinds }]
224
+ : [],
225
+ };
226
+ }
227
+
228
+ export function evaluateReleaseClaimCloseoutContract({
229
+ surface,
230
+ deliveryPosture = null,
231
+ releaseClaimPosture = 'repo_closeout',
232
+ observedKinds = [],
233
+ waivedKinds = [],
234
+ unsupportedClaims = [],
235
+ deferrals = [],
236
+ contradictionChecks = {},
237
+ } = {}) {
238
+ const posture = evaluateReleaseClaimPosture({
239
+ surface,
240
+ releaseClaimPosture,
241
+ observedKinds,
242
+ waivedKinds,
243
+ });
244
+ const missingContradictionChecks = CONTRADICTION_CATEGORIES.filter((name) => !(name in contradictionChecks));
245
+ const failedContradictionChecks = Object.entries(contradictionChecks)
246
+ .filter(([, status]) => status === 'failed')
247
+ .map(([name]) => name);
248
+ const unknownContradictionChecks = Object.keys(contradictionChecks)
249
+ .filter((name) => !CONTRADICTION_CATEGORIES.includes(name));
250
+ const invalidContradictionChecks = Object.entries(contradictionChecks)
251
+ .filter(([, status]) => !CONTRADICTION_STATUSES.includes(status))
252
+ .map(([name]) => name);
253
+ const blockingContradictionChecks = failedContradictionChecks.filter((name) =>
254
+ CONTRADICTION_BLOCKERS_BY_POSTURE[posture.releaseClaimPosture].includes(name)
255
+ );
256
+ const unresolvedUnsupportedClaims = unsupportedClaims.filter((claim) =>
257
+ !deferrals.some((deferral) => namesUnsupportedClaim(deferral, claim))
258
+ );
259
+ const blockers = [];
260
+
261
+ if (deliveryPosture && deliveryPosture !== posture.deliveryPosture) {
262
+ blockers.push({
263
+ code: 'incompatible_release_claim_posture',
264
+ details: [`${deliveryPosture} cannot support ${posture.releaseClaimPosture}; expected ${posture.deliveryPosture}`],
265
+ });
266
+ }
267
+
268
+ if (posture.missingKinds.length > 0) {
269
+ blockers.push({ code: 'missing_required_release_evidence', details: posture.missingKinds });
270
+ }
271
+ if (posture.invalidWaivers.length > 0) {
272
+ blockers.push({ code: 'invalid_release_waivers', details: posture.invalidWaivers });
273
+ }
274
+ if (unresolvedUnsupportedClaims.length > 0) {
275
+ blockers.push({ code: 'unsupported_release_claims', details: unresolvedUnsupportedClaims });
276
+ }
277
+ if (missingContradictionChecks.length > 0) {
278
+ blockers.push({ code: 'missing_release_contradiction_checks', details: missingContradictionChecks });
279
+ }
280
+ if (unknownContradictionChecks.length > 0) {
281
+ blockers.push({ code: 'unknown_release_contradiction_checks', details: unknownContradictionChecks });
282
+ }
283
+ if (invalidContradictionChecks.length > 0) {
284
+ blockers.push({ code: 'invalid_release_contradiction_checks', details: invalidContradictionChecks });
285
+ }
286
+ if (blockingContradictionChecks.length > 0) {
287
+ blockers.push({ code: 'failed_release_contradiction_checks', details: blockingContradictionChecks });
288
+ }
289
+
290
+ return {
291
+ ...posture,
292
+ unsupportedClaims: [...unsupportedClaims],
293
+ deferrals: [...deferrals],
294
+ failedContradictionChecks: blockingContradictionChecks,
295
+ allFailedContradictionChecks: failedContradictionChecks,
296
+ missingContradictionChecks,
297
+ unknownContradictionChecks,
298
+ invalidContradictionChecks,
299
+ unresolvedUnsupportedClaims,
300
+ blockers,
301
+ status: blockers.length === 0 ? 'supported' : 'unsupported',
111
302
  };
112
303
  }
304
+
305
+ function namesUnsupportedClaim(deferral, claim) {
306
+ const normalizedDeferral = normalizeClaimText(deferral);
307
+ const normalizedClaim = normalizeClaimText(claim);
308
+ if (!normalizedDeferral || !normalizedClaim) return false;
309
+ return normalizedDeferral.includes(normalizedClaim) && isStructuredDeferral(normalizedDeferral);
310
+ }
311
+
312
+ function isStructuredDeferral(normalizedDeferral) {
313
+ const namesEvidenceKind = EVIDENCE_KINDS.some((kind) => normalizedDeferral.includes(kind));
314
+ const namesLaterTarget = /\b(later|next|future|workflow|milestone|phase|gsdd)\b/.test(normalizedDeferral);
315
+ return namesEvidenceKind && namesLaterTarget;
316
+ }
317
+
318
+ function normalizeClaimText(value) {
319
+ return String(value || '')
320
+ .trim()
321
+ .toLowerCase()
322
+ .replace(/[^a-z0-9]+/g, ' ')
323
+ .replace(/\s+/g, ' ')
324
+ .trim();
325
+ }
@@ -56,8 +56,8 @@ export function runTruthChecks(planningDir, frameworkDir, actualCheckIds, option
56
56
  warnings.push({
57
57
  id: 'W8',
58
58
  severity: 'WARN',
59
- message: `distilled/README.md workflow inventory is out of sync (${issues.join('; ')})`,
60
- fix: 'Update distilled/README.md workflow status table and framework file tree to match distilled/workflows/',
59
+ message: `distilled/README.md workflow surface/status inventory is out of sync (${issues.join('; ')})`,
60
+ fix: 'Update distilled/README.md workflow inventory table and framework file tree to match distilled/workflows/',
61
61
  });
62
62
  }
63
63
  }
@@ -105,8 +105,8 @@ export function runTruthChecks(planningDir, frameworkDir, actualCheckIds, option
105
105
  id: 'W12',
106
106
  severity: 'WARN',
107
107
  message: `Planning state drifted since last recorded session (${drift.details.join('; ')})`,
108
- fix: 'Review the changes, then run a lifecycle workflow to update the fingerprint',
109
- });
108
+ fix: 'Review the changed planning files. If the drift is intentional, rebaseline with `node .planning/bin/gsdd.mjs session-fingerprint write`, then rerun the blocked lifecycle preflight.',
109
+ });
110
110
  }
111
111
 
112
112
  return warnings;
@@ -119,19 +119,34 @@ function extractHealthTableIds(content) {
119
119
  }
120
120
 
121
121
  function extractReadmeStatusEntries(content) {
122
- const section = extractSection(content, '## Current Status', 'Architecture notes:');
122
+ const workflowSurface = extractSection(content, '## Workflow Surface', 'Architecture notes:');
123
+ const currentStatus = extractSection(content, '## Current Status', 'Architecture notes:');
124
+ const section = workflowSurface || currentStatus;
123
125
  if (!section) return [];
124
126
  return [...normalizeContent(section).matchAll(/\|\s*`([^`]+\.md)`\s*\|/g)].map((result) => result[1]);
125
127
  }
126
128
 
127
129
  function extractReadmeWorkflowTreeEntries(content) {
128
130
  const section = extractSection(content, '## Files In This Framework', '## ');
129
- const match = normalizeContent(section || '').match(/```[\s\S]*?workflows\/\n([\s\S]*?)\n\s*templates\//);
131
+ const match = normalizeContent(section || '').match(/```[^\n]*\n([\s\S]*?)\n```/);
130
132
  if (!match) return [];
131
- return match[1]
132
- .split('\n')
133
- .map((line) => line.trim())
134
- .filter((line) => line.endsWith('.md'));
133
+ const entries = [];
134
+ let inWorkflows = false;
135
+ for (const rawLine of match[1].split('\n')) {
136
+ const line = rawLine.trim();
137
+ if (!line) continue;
138
+ if (!inWorkflows) {
139
+ if (/\bworkflows\/$/.test(line)) inWorkflows = true;
140
+ continue;
141
+ }
142
+ if (/\.md\b/.test(line)) {
143
+ const file = line.match(/([^\s/`|├└─]+\.md)\b/);
144
+ if (file) entries.push(file[1]);
145
+ continue;
146
+ }
147
+ if (/\b[^\s/]+\/$/.test(line)) break;
148
+ }
149
+ return entries;
135
150
  }
136
151
 
137
152
  function extractRepoLocalPaths(content) {
@@ -179,7 +179,7 @@ export function createCmdHealth(ctx) {
179
179
  const lifecycle = evaluateLifecycleState({ planningDir });
180
180
 
181
181
  if (roadmap && existsSync(phasesDir)) {
182
- for (const phase of lifecycle.phases.filter((entry) => entry.status !== 'not_started' && !entry.hasArtifacts)) {
182
+ for (const phase of lifecycle.phases.filter((entry) => entry.status !== 'not_started' && !entry.hasLifecycleArtifacts)) {
183
183
  warnings.push({
184
184
  id: 'W4',
185
185
  severity: 'WARN',
@@ -186,6 +186,7 @@ Commands:
186
186
  phase-status <N> <status> Update ROADMAP.md phase status ([ ] / [-] / [x])
187
187
  lifecycle-preflight <surface> [phase]
188
188
  Inspect deterministic lifecycle gate results for a workflow surface
189
+ session-fingerprint write Rebaseline planning-state drift after reviewing changed planning files
189
190
  help Show this summary
190
191
 
191
192
  Platforms (for --tools):
@@ -254,6 +255,7 @@ Starting lanes after init:
254
255
 
255
256
  Advanced/internal helpers (kept available, but not the primary first-run user story):
256
257
  lifecycle-preflight Inspect deterministic lifecycle gate results for a workflow surface
258
+ session-fingerprint Rebaseline the local planning-state fingerprint after review
257
259
  phase-status Update ROADMAP.md phase status through the local helper surface
258
260
  file-op Deterministic workspace-confined file copy/delete/text mutation
259
261
  `;