showdar-skills 0.9.0 → 0.10.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/CHANGELOG.md CHANGED
@@ -6,6 +6,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.10.0]
10
+
11
+ ### Added
12
+
13
+ - `showdar update-pack <path> --dry-run` read-only snapshot preview using the same canonical planning logic as execution; explicit public projection with deterministic ordering, descriptive change categories (`source-only`, `skill-content`, `workflow-definition`, `profile-definition`, `metadata`, `reference`, `ownership`, `installed-drift`), and `executable` status.
14
+ - Canonical internal pack update planning (`src/pack-plan.js`): `planPackUpdate()`, `verifyPlanPreconditions()`, `executePackUpdate()` with same-lifecycle TOCTOU fingerprint protection (source, installed owned files, relevant manifest state, overrides bytes). Stale plans abort with zero mutation.
15
+ - `showdar inspect-pack <path> --checkpoint <file>` checkpoint compatibility explanation against the candidate effective catalog (current project + candidate pack + project overrides). Deterministic semantic reason codes (`workflow-missing`, `workflow-state-compat-unsupported`, `stage-removed`, `selected-stage-invalid`, `required-stage-conflict`, `skip-policy-invalid`, `recorded-skip-invalid`) with fixed precedence; malformed checkpoints report `schema-invalid` / `malformed-checkpoint`.
16
+ - `showdar list --extensions --json` and `showdar doctor --extensions --json` using the common CLI envelope (`schemaVersion: 1`). Doctor reports `checkpointCompatibility: "not-assessed"` without a checkpoint. Diagnostics exit 0 on successful report.
17
+
18
+ ### Changed
19
+
20
+ - `src/pack-update.js` refactored around canonical planning; v0.9 staged replacement and rollback safety preserved.
21
+ - `list --extensions` human output now shows pack health status and pack profiles.
22
+ - `create-pack` skill scaffold cleaned up (full required sections, substantive guidance, no filler) with validation guidance output.
23
+ - TOCTOU lifecycle clarified: `--dry-run` is a read-only snapshot, not an authorization token. A later `update-pack` re-plans from current state. Stale-plan protection applies within a single execution lifecycle (`planPackUpdate` → `verifyPlanPreconditions` → `executePackUpdate`).
24
+
25
+ ### Fixed
26
+
27
+ - `inspectCustomWorkflows` read `validateCustomWorkflowDoc` as `{ok, errors}` but the validator returns an error array, so `valid` was always `undefined` and doctor reported every workflow invalid. Now maps `errors.length === 0` to `valid`. Internal helper shape only; public CLI output unchanged.
28
+ - `executePackUpdate` referenced undefined `manifestPath`; now resolves the project manifest path before writing. Covered by real update-path regression test.
29
+
30
+ ### Compatibility / Safety
31
+
32
+ - v0.9 `validate-pack --json` (`{ok, errors}`) and plain `inspect-pack --json` shapes unchanged; backward-compat fixture tests added.
33
+ - No persisted preview plans or tokens. No manifest v3, no workflow-state schema change, no WorkflowState key change.
34
+ - Phase 6G authority, 15 primitives, 4 workflows, 6 profiles, 10 trace events frozen.
35
+ - Dry-run is a snapshot, not a stored plan. No migration required. No tarball, remote registry, or executable plugins.
36
+
9
37
  ## [0.9.0]
10
38
 
11
39
  ### Added
package/README.md CHANGED
@@ -570,23 +570,47 @@ executable hooks, lifecycle scripts, or remote code.
570
570
  ```bash
571
571
  showdar create-pack <path> [--vendor <v>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]
572
572
  showdar validate-pack <local-path> [--json]
573
- showdar inspect-pack <local-path> [--json]
573
+ showdar inspect-pack <local-path> [--json] [--checkpoint <file>]
574
574
  ```
575
575
 
576
576
  **Install & lifecycle**
577
577
 
578
578
  ```bash
579
579
  showdar add-pack <local-path>
580
- showdar list --extensions
580
+ showdar list --extensions [--json]
581
581
  showdar remove-pack <name>
582
- showdar update-pack <local-path>
582
+ showdar update-pack <local-path> [--dry-run] [--json]
583
583
  ```
584
584
 
585
585
  **Diagnostics**
586
586
 
587
587
  ```bash
588
- showdar doctor --extensions
589
- ```
588
+ showdar doctor --extensions [--json]
589
+ ```
590
+
591
+ `update-pack --dry-run` previews changes without mutation: file add/replace/remove
592
+ counts, descriptive change categories (`source-only`, `skill-content`,
593
+ `workflow-definition`, `profile-definition`, `metadata`, `reference`,
594
+ `ownership`, `installed-drift`), ownership conflicts, and whether the update is
595
+ currently executable. Categories are descriptive only and never claim checkpoint
596
+ compatibility. The preview is a read-only snapshot, not an authorization token:
597
+ a later `update-pack` re-plans from current state. Within a single update
598
+ execution, if source, installed files, manifest, or overrides change between
599
+ planning and applying that same plan, execution aborts as a stale plan with
600
+ zero mutation.
601
+
602
+ `inspect-pack --checkpoint` validates a workflow checkpoint against the candidate
603
+ effective catalog (current project state + candidate pack + project overrides) and
604
+ reports `compatible`, `workflow-incompatible` (with deterministic reason code and
605
+ `replanRequired`), or `malformed`. Malformed checkpoints report `schema-invalid` /
606
+ `malformed-checkpoint`, never `workflow-incompatible`.
607
+
608
+ `list --extensions --json` and `doctor --extensions --json` use the common CLI
609
+ envelope (`schemaVersion: 1`, `command`, `ok`, `data`, `warnings`, `errors`).
610
+ Existing `validate-pack --json` and plain `inspect-pack --json` shapes are
611
+ unchanged. `doctor` without a checkpoint reports `checkpointCompatibility:
612
+ "not-assessed"`. Diagnostics exit 0 when they successfully report state, even when
613
+ unhealthy; validation and execution failures exit 1.
590
614
 
591
615
  **Pack metadata**
592
616
 
package/bin/showdar.js CHANGED
@@ -4,7 +4,9 @@ import { homedir } from 'node:os';
4
4
  import { readFile } from 'node:fs/promises';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { AI_TARGETS, PRIMITIVE_COUNT, PROFILE_ALIASES, PROFILES, SKILLS, TOTAL_COUNT, WORKFLOW_COUNT, canonicalProfile, isDeprecatedProfile, resolveProfile } from '../src/catalog.js';
7
- import { addPack, addSkill, addWorkflow, globalManifestPath, initGlobal, initProject, inspectGlobal, inspectProject, listExtensions, removeGlobal, removePack, removeProject, validatePackSource, createPack, inspectPack, doctor, updatePack, readProjectOverrides } from '../src/project.js';
7
+ import { addPack, addSkill, addWorkflow, globalManifestPath, initGlobal, initProject, inspectGlobal, inspectProject, listExtensions, listExtensionsDetailed, removeGlobal, removePack, removeProject, validatePackSource, createPack, inspectPack, doctor, updatePack, readProjectOverrides } from '../src/project.js';
8
+ import { formatPlanPreviewHuman, projectPackUpdatePreview } from '../src/pack-plan.js';
9
+ import { assessCheckpointAgainstCandidate } from '../src/pack-inspect.js';
8
10
  import { validateRepository } from '../src/validate.js';
9
11
 
10
12
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -43,7 +45,7 @@ function printHelp(version, command = null) {
43
45
  console.log(`Showdar Skills ${version}\n\nUsage:\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n\nExamples:\n showdar add debug\n showdar add showdar-security\n showdar add test --ai cursor\n showdar add review --scope global --ai claude\n\nDefault scope: project. Default AI target: universal, or the configured .showdar.json value when present.`);
44
46
  return;
45
47
  }
46
- console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>] [--pack <local-path>]\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n showdar add-pack <local-path>\n showdar remove-pack <name>\n showdar add-workflow <local-path>\n showdar status ${scopeUsage}\n showdar doctor ${scopeUsage}\n showdar validate\n showdar list [--extensions]\n showdar remove ${scopeUsage}\n showdar create-pack <path> [--vendor <vendor>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]\n showdar validate-pack <local-path> [--json]\n showdar inspect-pack <local-path> [--json]\n showdar doctor ${scopeUsage}\n showdar update-pack <local-path>\n showdar validate\n showdar list [--extensions]\n showdar remove ${scopeUsage}\n\nExtension packs accept local directories/workspace paths only; tarball, URL, Git, and registry sources are rejected.\n\nDefaults: scope project, profile full, AI target universal.\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
48
+ console.log(`Showdar Skills ${version}\n\nUsage:\n showdar init ${scopeUsage} [--profile <name>] [--ai <universal|codex|opencode|cursor|claude|all>] [--pack <local-path>]\n showdar add <skill> [--ai <universal|codex|opencode|cursor|claude>] [--scope <project|global>]\n showdar add-pack <local-path>\n showdar remove-pack <name>\n showdar add-workflow <local-path>\n showdar status ${scopeUsage}\n showdar doctor ${scopeUsage}\n showdar validate\n showdar list [--extensions]\n showdar remove ${scopeUsage}\n showdar create-pack <path> [--vendor <vendor>] [--description <text>] [--with-workflow <id>] [--with-profile <name>]\n showdar validate-pack <local-path> [--json]\n showdar inspect-pack <local-path> [--json] [--checkpoint <file>]\n showdar doctor ${scopeUsage} [--extensions] [--json]\n showdar update-pack <local-path> [--dry-run] [--json]\n showdar validate\n showdar list [--extensions] [--json]\n showdar remove ${scopeUsage}\n\nExtension packs accept local directories/workspace paths only; tarball, URL, Git, and registry sources are rejected.\n\nDefaults: scope project, profile full, AI target universal.\nProfiles: ${Object.keys(PROFILES).join(', ')}\nDeprecated aliases: ${Object.entries(PROFILE_ALIASES).map(([alias, target]) => `${alias} -> ${target}`).join(', ')}\nAI targets: ${AI_TARGETS.join(', ')}`);
47
49
  }
48
50
 
49
51
  async function main() {
@@ -63,11 +65,20 @@ async function main() {
63
65
 
64
66
  if (command === 'list') {
65
67
  if (args.includes('--extensions')) {
66
- const result = await listExtensions({ cwd: projectRoot });
68
+ const isJson = args.includes('--json');
69
+ const result = await listExtensionsDetailed({ cwd: projectRoot });
70
+ if (isJson) {
71
+ console.log(JSON.stringify({ schemaVersion: 1, command: 'list', ok: true, data: result, warnings: [], errors: [] }, null, 2));
72
+ return;
73
+ }
67
74
  console.log('Packs:');
68
- for (const pack of result.packs) console.log(` ${pack.name}@${pack.version} ${pack.hash}`);
75
+ for (const pack of result.packs) console.log(` ${pack.name}@${pack.version} ${pack.status ?? 'healthy'} ${pack.hash}`);
69
76
  console.log('Custom workflows:');
70
77
  for (const workflow of result.customWorkflows) console.log(` ${workflow.id} [${workflow.source}] ${workflow.path}`);
78
+ if (result.profiles?.length) {
79
+ console.log('Pack profiles:');
80
+ for (const profile of result.profiles) console.log(` ${profile.name} [${profile.source}] members: ${(profile.members ?? []).join(', ')}`);
81
+ }
71
82
  console.log(`Project overrides: ${result.overrides.present ? result.overrides.status : 'absent'}`);
72
83
  return;
73
84
  }
@@ -140,6 +151,31 @@ async function main() {
140
151
  }
141
152
 
142
153
  if (command === 'status' || command === 'doctor') {
154
+ if (command === 'doctor' && args.includes('--extensions')) {
155
+ const isJson = args.includes('--json');
156
+ const result = await doctor({ cwd: projectRoot });
157
+ if (isJson) {
158
+ console.log(JSON.stringify({
159
+ schemaVersion: 1,
160
+ command: 'doctor',
161
+ ok: true,
162
+ data: { ...result, checkpointCompatibility: 'not-assessed' },
163
+ warnings: result.warnings ?? [],
164
+ errors: [],
165
+ }, null, 2));
166
+ return;
167
+ }
168
+ console.log(`Extension diagnostics\nHealth: ${result.healthy ? 'OK' : 'BROKEN'}`);
169
+ for (const pack of result.packs) console.log(` ${pack.name}@${pack.version} ${pack.drift}`);
170
+ for (const wf of result.customWorkflows) console.log(` workflow ${wf.id}: ${wf.valid ? 'valid' : 'invalid'}`);
171
+ for (const issue of result.issues) console.log(`- ${issue}`);
172
+ for (const warning of result.warnings ?? []) console.log(`warning: ${warning}`);
173
+ if (!result.healthy) {
174
+ console.log(`Checkpoint compatibility: not-assessed (supply a checkpoint via inspect-pack --checkpoint)`);
175
+ if (result.customWorkflows.length) console.log(`Note: checkpoints referencing changed workflow definitions will be revalidated on resume.`);
176
+ }
177
+ return;
178
+ }
143
179
  const result = scope === 'global' ? await inspectGlobal() : await inspectProject(projectRoot);
144
180
  if (!result.installed) {
145
181
  console.log(`Showdar Skills is not installed in the ${scope} scope.`);
@@ -213,8 +249,31 @@ async function main() {
213
249
 
214
250
  if (command === 'inspect-pack') {
215
251
  const packPath = args[1];
216
- if (!packPath) throw new Error('Pack path is required. Usage: showdar inspect-pack <local-path> [--json]');
252
+ if (!packPath) throw new Error('Pack path is required. Usage: showdar inspect-pack <local-path> [--json] [--checkpoint <file>]');
217
253
  const isJson = args.includes('--json');
254
+ const checkpointIdx = args.indexOf('--checkpoint');
255
+ const checkpointFile = checkpointIdx === -1 ? null : args[checkpointIdx + 1];
256
+ if (checkpointIdx !== -1 && (!checkpointFile || checkpointFile.startsWith('--'))) {
257
+ throw new Error('--checkpoint requires a file path');
258
+ }
259
+ if (checkpointFile) {
260
+ const { readFile } = await import('node:fs/promises');
261
+ const checkpoint = await readFile(path.resolve(projectRoot, checkpointFile), 'utf8');
262
+ const assessment = await assessCheckpointAgainstCandidate({ cwd: projectRoot, candidatePath: path.resolve(projectRoot, packPath), checkpoint });
263
+ if (isJson) {
264
+ console.log(JSON.stringify({ schemaVersion: 1, command: 'inspect-pack', ok: true, data: { checkpoint: assessment }, warnings: [], errors: [] }, null, 2));
265
+ return;
266
+ }
267
+ if (assessment.compatible) {
268
+ console.log(`Checkpoint compatibility: compatible\nReplan required: no`);
269
+ return;
270
+ }
271
+ console.log(`Checkpoint compatibility: ${assessment.category === 'schema-invalid' ? 'malformed' : 'workflow-incompatible'}\nReplan required: ${assessment.replanRequired ? 'yes' : 'no'}\nCategory: ${assessment.category}\nReason: ${assessment.reason}\nDetail: ${assessment.detail}`);
272
+ if (assessment.affectedWorkflow) console.log(`Affected workflow: ${assessment.affectedWorkflow}`);
273
+ if (assessment.affectedStage) console.log(`Affected stage: ${assessment.affectedStage}`);
274
+ console.log(`\nRun \`showdar update-pack <path> --dry-run\` to preview update.`);
275
+ return;
276
+ }
218
277
  const result = await inspectPack({ cwd: projectRoot, source: packPath });
219
278
  if (isJson) {
220
279
  console.log(JSON.stringify(result, null, 2));
@@ -246,9 +305,38 @@ async function main() {
246
305
 
247
306
  if (command === 'update-pack') {
248
307
  const packSource = args[1];
249
- if (!packSource) throw new Error('Pack source is required. Usage: showdar update-pack <local-path>');
250
- const result = await updatePack({ cwd: projectRoot, source: packSource });
251
- console.log(`Showdar pack ${result.status}.\nPack: ${result.pack}\nVersion: ${result.version}${result.oldHash ? `\nOld hash: ${result.oldHash}\nNew hash: ${result.newHash}` : ''}\nFiles: ${result.files}`);
308
+ if (!packSource) throw new Error('Pack source is required. Usage: showdar update-pack <local-path> [--dry-run] [--json]');
309
+ const dryRun = args.includes('--dry-run');
310
+ const isJson = args.includes('--json');
311
+ if (dryRun) {
312
+ const { planPackUpdate } = await import('../src/pack-plan.js');
313
+ const plan = await planPackUpdate({ cwd: projectRoot, source: packSource });
314
+ if (isJson) {
315
+ console.log(JSON.stringify(projectPackUpdatePreview(plan), null, 2));
316
+ if (!plan.ok || !plan.executable) process.exitCode = 1;
317
+ return;
318
+ }
319
+ console.log(formatPlanPreviewHuman(plan));
320
+ if (!plan.ok || !plan.executable) process.exitCode = 1;
321
+ return;
322
+ }
323
+ if (isJson) {
324
+ try {
325
+ const result = await updatePack({ cwd: projectRoot, source: packSource });
326
+ console.log(JSON.stringify({ schemaVersion: 1, command: 'update-pack', ok: true, data: result, warnings: result.plan?.warnings ?? [], errors: [] }, null, 2));
327
+ } catch (error) {
328
+ console.log(JSON.stringify({ schemaVersion: 1, command: 'update-pack', ok: false, data: null, warnings: [], errors: [{ category: 'drift-detected', code: 'DRIFT_DETECTED', message: error.message }] }, null, 2));
329
+ process.exitCode = 1;
330
+ }
331
+ return;
332
+ }
333
+ try {
334
+ const result = await updatePack({ cwd: projectRoot, source: packSource });
335
+ console.log(`Showdar pack ${result.status}.\nPack: ${result.pack}\nVersion: ${result.version}${result.oldHash ? `\nOld hash: ${result.oldHash}\nNew hash: ${result.newHash}` : ''}\nFiles: ${result.files}`);
336
+ } catch (error) {
337
+ console.error(`showdar: ${error.message}`);
338
+ process.exitCode = 1;
339
+ }
252
340
  return;
253
341
  }
254
342
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "showdar-skills",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Production-grade software engineering lifecycle skills for coding agents.",
5
5
  "type": "module",
6
6
  "bin": { "showdar": "./bin/showdar.js" },
@@ -0,0 +1,197 @@
1
+ import { createExtensionCatalog } from './extension-catalog.js';
2
+ import { validateWorkflowState } from './workflow-state.js';
3
+
4
+ export const COMPATIBILITY_REASONS = Object.freeze({
5
+ WORKFLOW_MISSING: 'workflow-missing',
6
+ WORKFLOW_STATE_COMPAT_UNSUPPORTED: 'workflow-state-compat-unsupported',
7
+ STAGE_REMOVED: 'stage-removed',
8
+ SELECTED_STAGE_INVALID: 'selected-stage-invalid',
9
+ REQUIRED_STAGE_CONFLICT: 'required-stage-conflict',
10
+ SKIP_POLICY_INVALID: 'skip-policy-invalid',
11
+ RECORDED_SKIP_INVALID: 'recorded-skip-invalid',
12
+ });
13
+
14
+ export const MALFORMED_REASON = 'malformed-checkpoint';
15
+
16
+ const REASON_PRECEDENCE = Object.freeze([
17
+ 'workflow-missing',
18
+ 'workflow-state-compat-unsupported',
19
+ 'stage-removed',
20
+ 'selected-stage-invalid',
21
+ 'required-stage-conflict',
22
+ 'skip-policy-invalid',
23
+ 'recorded-skip-invalid',
24
+ ]);
25
+
26
+ const REMEDIATION_MESSAGES = Object.freeze({
27
+ 'workflow-missing': 'Workflow no longer exists in the current candidate catalog.',
28
+ 'workflow-state-compat-unsupported': 'Checkpoint uses unsupported workflowStateCompat schema version.',
29
+ 'stage-removed': 'Selected stage is no longer declared by the workflow.',
30
+ 'selected-stage-invalid': 'Selected stage is not a valid candidate stage.',
31
+ 'required-stage-conflict': 'Required stage is not selected.',
32
+ 'skip-policy-invalid': 'Skip rule for stage is not in workflow policy.',
33
+ 'recorded-skip-invalid': 'Recorded skip reason or policy is no longer accepted.',
34
+ 'malformed-checkpoint': 'Checkpoint cannot be parsed or fails schema validation.',
35
+ });
36
+
37
+ function isRecord(value) {
38
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
39
+ }
40
+
41
+ export async function assessCompatibilityReasons(checkpoint, catalog) {
42
+ let parsed = checkpoint;
43
+ if (typeof checkpoint === 'string') {
44
+ try {
45
+ parsed = JSON.parse(checkpoint);
46
+ } catch {
47
+ return {
48
+ compatible: false,
49
+ replanRequired: true,
50
+ category: 'schema-invalid',
51
+ reason: MALFORMED_REASON,
52
+ detail: REMEDIATION_MESSAGES[MALFORMED_REASON],
53
+ affectedWorkflow: null,
54
+ affectedStage: null,
55
+ };
56
+ }
57
+ }
58
+
59
+ if (!isRecord(parsed)) {
60
+ return {
61
+ compatible: false,
62
+ replanRequired: true,
63
+ category: 'schema-invalid',
64
+ reason: MALFORMED_REASON,
65
+ detail: REMEDIATION_MESSAGES[MALFORMED_REASON],
66
+ affectedWorkflow: null,
67
+ affectedStage: null,
68
+ };
69
+ }
70
+
71
+ const workflowId = parsed.workflowId;
72
+ const catalogStages = catalog?.selectableStages?.[workflowId] ?? null;
73
+ if (typeof workflowId !== 'string' || !catalogStages) {
74
+ return {
75
+ compatible: false,
76
+ replanRequired: true,
77
+ category: 'workflow-incompatible',
78
+ reason: 'workflow-missing',
79
+ detail: `${REMEDIATION_MESSAGES['workflow-missing']} Workflow: ${workflowId ?? 'unknown'}`,
80
+ affectedWorkflow: workflowId ?? null,
81
+ affectedStage: null,
82
+ };
83
+ }
84
+
85
+ const validation = validateWorkflowState(parsed, { extensionCatalog: catalog });
86
+ if (validation.ok) {
87
+ return {
88
+ compatible: true,
89
+ replanRequired: false,
90
+ category: null,
91
+ reason: null,
92
+ detail: null,
93
+ affectedWorkflow: workflowId,
94
+ affectedStage: null,
95
+ };
96
+ }
97
+
98
+ const errors = validation.errors;
99
+ const reason = mapErrorsToReason(errors, parsed, catalog);
100
+ return {
101
+ compatible: false,
102
+ replanRequired: true,
103
+ category: 'workflow-incompatible',
104
+ reason: reason.code,
105
+ detail: reason.detail,
106
+ affectedWorkflow: workflowId,
107
+ affectedStage: reason.stage,
108
+ };
109
+ }
110
+
111
+ function mapErrorsToReason(errors, parsed, catalog) {
112
+ const errorText = errors.join('; ');
113
+ const workflowId = parsed.workflowId;
114
+
115
+ if (errorText.includes('workflowId must be one of')) {
116
+ return { code: 'workflow-missing', stage: null, detail: `${REMEDIATION_MESSAGES['workflow-missing']} Workflow: ${workflowId}` };
117
+ }
118
+
119
+ const doc = findWorkflowDoc(catalog, workflowId);
120
+ if (doc && doc.workflowStateCompat && doc.workflowStateCompat.schemaVersion !== 1) {
121
+ return { code: 'workflow-state-compat-unsupported', stage: null, detail: REMEDIATION_MESSAGES['workflow-state-compat-unsupported'] };
122
+ }
123
+
124
+ const stages = catalog.selectableStages[workflowId] ?? [];
125
+ for (const stage of parsed.selectedStages ?? []) {
126
+ if (!stages.includes(stage)) {
127
+ return {
128
+ code: 'stage-removed',
129
+ stage,
130
+ detail: `${REMEDIATION_MESSAGES['stage-removed']} Stage: ${stage}, workflow: ${workflowId}`,
131
+ };
132
+ }
133
+ }
134
+
135
+ for (const stage of parsed.selectedStages ?? []) {
136
+ if (!(parsed.candidateStages ?? []).includes(stage)) {
137
+ return {
138
+ code: 'selected-stage-invalid',
139
+ stage,
140
+ detail: `${REMEDIATION_MESSAGES['selected-stage-invalid']} Stage: ${stage}`,
141
+ };
142
+ }
143
+ }
144
+
145
+ for (const error of errors) {
146
+ if (error.includes('required stage')) {
147
+ const m = error.match(/required stage (\S+)/);
148
+ return {
149
+ code: 'required-stage-conflict',
150
+ stage: m ? m[1] : null,
151
+ detail: `${REMEDIATION_MESSAGES['required-stage-conflict']} ${error}`,
152
+ };
153
+ }
154
+ }
155
+
156
+ for (const error of errors) {
157
+ if (error.includes('not skippable') || error.includes('skipRules') || error.includes('skip rule')) {
158
+ return {
159
+ code: 'skip-policy-invalid',
160
+ stage: extractStage(error),
161
+ detail: `${REMEDIATION_MESSAGES['skip-policy-invalid']} ${error}`,
162
+ };
163
+ }
164
+ }
165
+
166
+ for (const error of errors) {
167
+ if (error.includes('skip reason') || error.includes('skip policy')) {
168
+ return {
169
+ code: 'recorded-skip-invalid',
170
+ stage: extractStage(error),
171
+ detail: `${REMEDIATION_MESSAGES['recorded-skip-invalid']} ${error}`,
172
+ };
173
+ }
174
+ }
175
+
176
+ return {
177
+ code: 'recorded-skip-invalid',
178
+ stage: null,
179
+ detail: `${REMEDIATION_MESSAGES['recorded-skip-invalid']} ${errors[0] ?? 'Unknown incompatibility'}`,
180
+ };
181
+ }
182
+
183
+ function findWorkflowDoc(catalog, workflowId) {
184
+ return null;
185
+ }
186
+
187
+ function extractStage(error) {
188
+ const m = error.match(/stage (\S+)/) || error.match(/for (\S+)/);
189
+ return m ? m[1].replace(/[.,;:'"]/g, '') : null;
190
+ }
191
+
192
+ export function validateReasonPrecedence(reasons) {
193
+ const indices = reasons.map((r) => REASON_PRECEDENCE.indexOf(r));
194
+ return indices.every((idx, i) => i === 0 || indices[i - 1] <= idx);
195
+ }
196
+
197
+ export { REASON_PRECEDENCE, REMEDIATION_MESSAGES };
@@ -181,9 +181,9 @@ async function inspectCustomWorkflows(cwd, manifest) {
181
181
  try {
182
182
  const docPath = path.join(cwd, wf.path);
183
183
  const doc = JSON.parse(await readFile(docPath, 'utf8'));
184
- const { validateCustomWorkflowDoc, validateCustomWorkflowId } = await import('./validate-pack.js');
185
- const validation = validateCustomWorkflowDoc(doc, `workflow ${doc.id}`);
186
- results.push({ id: wf.id, path: wf.path, source: wf.source, valid: validation.ok, errors: validation.errors });
184
+ const { validateCustomWorkflowDoc } = await import('./validate-pack.js');
185
+ const errors = validateCustomWorkflowDoc(doc, `workflow ${doc.id}`);
186
+ results.push({ id: wf.id, path: wf.path, source: wf.source, valid: errors.length === 0, errors });
187
187
  } catch (error) {
188
188
  results.push({ id: wf.id, path: wf.path, source: wf.source, valid: false, errors: [error.message] });
189
189
  }
@@ -257,32 +257,105 @@ async function assessWorkflowCompatibility(checkpoint, extensionCatalog) {
257
257
  async function listExtensionsWithDetails(cwd) {
258
258
  const manifest = await readManifest(path.join(cwd, '.showdar.json'), cwd);
259
259
  if (!manifest) return null;
260
-
260
+
261
261
  const overrides = await import('./project.js').then(m => m.readProjectOverrides({ cwd })).catch(() => null);
262
262
  const overridesStatus = overrides ? 'valid' : 'absent';
263
-
263
+
264
264
  const packs = [];
265
265
  for (const pack of manifest.extensions?.packs ?? []) {
266
266
  const installed = await inspectPackInstalled(cwd, pack.name, pack);
267
- packs.push({ name: pack.name, version: pack.version, hash: pack.hash, drift: installed.drift, driftDetails: installed.driftDetails });
267
+ const status = installed.drift === 'no-drift' ? 'healthy'
268
+ : installed.drift === 'source-unavailable' ? 'source-unavailable'
269
+ : installed.drift === 'installed-file-drift' ? 'installed-drift'
270
+ : installed.drift === 'source-drift' ? 'source-drift'
271
+ : installed.drift;
272
+ packs.push({ name: pack.name, version: pack.version, hash: pack.hash, status, drift: installed.drift, driftDetails: installed.driftDetails });
268
273
  }
269
-
274
+
270
275
  const customWorkflows = [];
271
276
  for (const wf of manifest.extensions?.customWorkflows ?? []) {
272
277
  customWorkflows.push({ id: wf.id, source: wf.source, path: wf.path });
273
278
  }
274
-
275
- return { packs, customWorkflows, overrides: { present: true, status: overridesStatus } };
279
+
280
+ const projectProfiles = [];
281
+ if (overrides?.profiles) {
282
+ for (const name of Object.keys(overrides.profiles)) {
283
+ projectProfiles.push({ name, source: 'project-override', members: overrides.profiles[name] });
284
+ }
285
+ }
286
+ for (const pack of manifest.extensions?.packs ?? []) {
287
+ const packDir = path.join(cwd, EXTENSION_DIR, 'packs', pack.name);
288
+ try {
289
+ const packManifest = JSON.parse(await readFile(path.join(packDir, 'pack.json'), 'utf8'));
290
+ for (const [name, members] of Object.entries(packManifest.profiles ?? {})) {
291
+ projectProfiles.push({ name, source: `pack:${pack.name}`, members });
292
+ }
293
+ } catch {}
294
+ }
295
+
296
+ return { packs, customWorkflows, profiles: projectProfiles, overrides: { present: overrides !== null, status: overridesStatus } };
276
297
  }
277
298
 
278
- export {
279
- inspectPackSource,
280
- inspectPackInstalled,
281
- inspectCustomWorkflows,
299
+ async function buildCandidateEffectiveCatalog({ cwd, candidatePath }) {
300
+ const { createExtensionCatalog } = await import('./extension-catalog.js');
301
+ const manifestPath = path.join(cwd, '.showdar.json');
302
+ const manifest = await readManifest(manifestPath, cwd).catch(() => null);
303
+
304
+ const candidateManifest = await readPackManifest(candidatePath);
305
+ const candidateWorkflows = {};
306
+ for (const wf of candidateManifest.workflows ?? []) {
307
+ try {
308
+ candidateWorkflows[wf.id] = JSON.parse(await readFile(path.join(candidatePath, wf.path), 'utf8'));
309
+ } catch {}
310
+ }
311
+
312
+ const packs = [];
313
+ if (manifest) {
314
+ for (const pack of manifest.extensions?.packs ?? []) {
315
+ if (pack.name === candidateManifest.name) continue;
316
+ const packDir = path.join(cwd, EXTENSION_DIR, 'packs', pack.name);
317
+ try {
318
+ const pm = JSON.parse(await readFile(path.join(packDir, 'pack.json'), 'utf8'));
319
+ const workflows = {};
320
+ for (const wf of pm.workflows ?? []) {
321
+ try {
322
+ workflows[wf.id] = JSON.parse(await readFile(path.join(packDir, wf.path), 'utf8'));
323
+ } catch {}
324
+ }
325
+ packs.push({ manifest: pm, workflows });
326
+ } catch {}
327
+ }
328
+ }
329
+
330
+ packs.push({ manifest: candidateManifest, workflows: candidateWorkflows });
331
+
332
+ let projectOverrides = null;
333
+ try {
334
+ const { readProjectOverrides } = await import('./project.js');
335
+ projectOverrides = await readProjectOverrides({ cwd });
336
+ } catch {}
337
+
338
+ const result = createExtensionCatalog({ packs, projectOverrides });
339
+ if (!result.ok) throw new Error(`Candidate catalog invalid: ${result.errors.join('; ')}`);
340
+ return result.value;
341
+ }
342
+
343
+ async function assessCheckpointAgainstCandidate({ cwd, candidatePath, checkpoint }) {
344
+ const { assessCompatibilityReasons } = await import('./pack-compat.js');
345
+ const catalog = await buildCandidateEffectiveCatalog({ cwd, candidatePath });
346
+ return assessCompatibilityReasons(checkpoint, catalog);
347
+ }
348
+
349
+ export {
350
+ inspectPackSource,
351
+ inspectPackInstalled,
352
+ inspectCustomWorkflows,
282
353
  inspectOverrides,
283
354
  computePrecedence,
284
355
  assessWorkflowCompatibility,
285
356
  listExtensionsWithDetails,
357
+ buildCandidateEffectiveCatalog,
358
+ assessCheckpointAgainstCandidate,
286
359
  DRIFT_CATEGORIES,
287
360
  };
288
361
 
@@ -293,6 +366,8 @@ export const packInspectAPI = {
293
366
  computePrecedence,
294
367
  assessWorkflowCompatibility,
295
368
  listExtensionsWithDetails,
369
+ buildCandidateEffectiveCatalog,
370
+ assessCheckpointAgainstCandidate,
296
371
  };
297
372
 
298
373
  export default { inspectPackSource, inspectPackInstalled };