showdar-skills 0.6.0 → 0.7.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
@@ -4,6 +4,37 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [0.7.0]
8
+
9
+ ### Added
10
+
11
+ - Deterministic workflow trace projection (`src/workflow-trace.js`): pure
12
+ state-diff observation over workflow transitions with 10 closed semantic
13
+ event types, no timestamps, no authority content, and no automatic
14
+ persistence.
15
+ - Workflow benchmark scenario schema and loader
16
+ (`benchmark/schema/workflow-scenario.schema.json`,
17
+ `benchmark/lib/workflow-scenario-loader.js`).
18
+ - Deterministic workflow benchmark corpus
19
+ (`benchmark/scenarios/workflows/`, 16 scenarios): exact-match M1–M10
20
+ invariants covering selection, skip policy, verification preservation,
21
+ stale-resume blocking, authority invariance, and completion.
22
+ - `npm run eval:workflows` driver
23
+ (`scripts/workflow-observability-eval.mjs`).
24
+
25
+ ### Changed
26
+
27
+ - `npm run eval` now runs retrieval evaluation (`eval:retrieval`) followed
28
+ by workflow evaluation (`eval:workflows`); release evaluation blocks on
29
+ exact workflow invariants.
30
+ - `npm run check` remains test/validate/package correctness only.
31
+
32
+ ### Safety
33
+
34
+ - Traces contain normalized semantic data only (IDs, enums, receipt
35
+ summaries, statuses): no authority state, raw prompts, logs, telemetry,
36
+ or network reporting.
37
+
7
38
  ## [0.6.0]
8
39
 
9
40
  ### Added
package/MIGRATION.md CHANGED
@@ -1,3 +1,20 @@
1
+ # Migrating to 0.7.0
2
+
3
+ 0.7.0 adds deterministic workflow trace projection
4
+ (`src/workflow-trace.js`) and a workflow benchmark corpus over the
5
+ unchanged 0.6 runtime. No user action is required.
6
+
7
+ - Existing 0.6 installs and configs remain valid: no workflow-state
8
+ schema migration (still schemaVersion 1), no config migration, no
9
+ adapter migration, no checkpoint migration.
10
+ - Runtime workflow semantics are unchanged; traces observe released
11
+ behavior only and never affect execution, routing, or authority.
12
+ - The new `src/workflow-trace.js` module ships in the package; benchmark
13
+ scenarios, schema, loader, and eval driver are development/release
14
+ tooling and do not ship.
15
+ - No telemetry, network reporting, automatic storage, or tracking is
16
+ introduced.
17
+
1
18
  # Migrating to 0.6.0
2
19
 
3
20
  0.6.0 adds portable workflow execution state (`src/workflow-state.js`,
package/README.md CHANGED
@@ -443,6 +443,39 @@ Per-workflow skip policy (evidence-backed, never severity or wording alone):
443
443
  - Incident: severity never grants production mutation; recovery and
444
444
  verification remain gated by current authority.
445
445
 
446
+ ### Workflow traces (observability, benchmark-only)
447
+
448
+ Workflow traces are pure projections of before/after workflow states
449
+ (`src/workflow-trace.js`): ordered events such as `stage-entered`,
450
+ `stage-completed`, `stage-skipped`, `workflow-blocked`, `workflow-resumed`,
451
+ and `workflow-completed`. Traces carry stage IDs, skip reasons, receipt
452
+ summaries, and statuses only — no timestamps, no prompts, no secrets, no
453
+ authority content. Nothing persists them automatically; the benchmark
454
+ corpus (`npm run eval:workflows`) uses them to verify selection, skip,
455
+ resume, and completion behavior deterministically.
456
+
457
+ A workflow trace does not mutate workflow state, affect routing or
458
+ authority, persist automatically, or send telemetry. The 10 event
459
+ categories are `workflow-created`, `stages-selected`, `stage-entered`,
460
+ `evidence-recorded`, `stage-completed`, `stage-skipped`,
461
+ `workflow-blocked`, `workflow-interrupted`, `workflow-resumed`, and
462
+ `workflow-completed`.
463
+
464
+ ### Evaluation
465
+
466
+ ```bash
467
+ npm run eval:retrieval # retrieval evaluation (unchanged behavior)
468
+ npm run eval:workflows # deterministic workflow semantic benchmark
469
+ npm run eval # both, sequentially (release-blocking)
470
+ ```
471
+
472
+ Workflow evaluation asserts exact M1–M10 invariants (selection accuracy,
473
+ invalid-skip rejection, verification preservation, stale-resume blocking,
474
+ authority invariance, completion, trace equality, revision monotonicity,
475
+ checkpoint round-trip, skip-evidence backing) with no fuzzy score
476
+ thresholds. `npm run check` (test/validate/pack) does not run the
477
+ benchmark; the release pipeline runs `npm run eval`, gating both suites.
478
+
446
479
  ## A typical software workflow
447
480
 
448
481
  ```text
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "showdar-skills",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Production-grade software engineering lifecycle skills for coding agents.",
5
5
  "type": "module",
6
6
  "bin": { "showdar": "./bin/showdar.js" },
@@ -8,7 +8,9 @@
8
8
  "test": "node --test",
9
9
  "validate": "node bin/showdar.js validate",
10
10
  "sync:runtime": "node scripts/sync-runtime.mjs",
11
- "eval": "node scripts/retrieval-eval.mjs",
11
+ "eval": "npm run eval:retrieval && npm run eval:workflows",
12
+ "eval:retrieval": "node scripts/retrieval-eval.mjs",
13
+ "eval:workflows": "node scripts/workflow-observability-eval.mjs",
12
14
  "check": "npm test && npm run validate && npm pack --dry-run",
13
15
  "smoke": "node scripts/package-smoke.mjs",
14
16
  "release:check": "node scripts/check-release-version.mjs"
package/src/validate.js CHANGED
@@ -266,6 +266,64 @@ export async function validateWorkflowStatePolicy(packageRoot) {
266
266
  for (const storage of ['.showdar/state', '~/.showdar', 'sqlite', 'telemetry']) {
267
267
  if (moduleText.toLowerCase().includes(storage.toLowerCase())) errors.push(`workflow-state: no storage layer reference ${storage}`);
268
268
  }
269
+ for (const error of (await validateWorkflowScenarioCoverage(packageRoot, policy)).errors) errors.push(error);
270
+ return { ok: errors.length === 0, errors };
271
+ }
272
+
273
+ export async function validateWorkflowScenarioCoverage(packageRoot, policy = null) {
274
+ const errors = [];
275
+ const primitives = new Set(SKILLS.map((skill) => skill.id));
276
+ const workflows = new Set(WORKFLOW_SKILLS.map((w) => w.id));
277
+ let trace;
278
+ try {
279
+ trace = await import('./workflow-trace.js');
280
+ } catch (error) {
281
+ return { ok: false, errors: [`workflow-trace unavailable: ${error.message}`] };
282
+ }
283
+ const eventTypes = new Set(trace.WORKFLOW_EVENT_TYPES);
284
+ let files;
285
+ try {
286
+ files = await readdir(path.join(packageRoot, 'benchmark', 'scenarios', 'workflows'));
287
+ } catch {
288
+ return { ok: false, errors: ['workflow-scenarios: benchmark/scenarios/workflows missing'] };
289
+ }
290
+ const scenarioFiles = files.filter((f) => f.endsWith('.json')).sort();
291
+ if (!scenarioFiles.length) return { ok: false, errors: ['workflow-scenarios: no scenario files'] };
292
+ const seenIds = new Set();
293
+ const coverage = new Map();
294
+ for (const file of scenarioFiles) {
295
+ let scenario;
296
+ try {
297
+ scenario = JSON.parse(await readFile(path.join(packageRoot, 'benchmark', 'scenarios', 'workflows', file), 'utf8'));
298
+ } catch (error) {
299
+ errors.push(`workflow-scenarios: ${file} is not valid JSON (${error.message})`);
300
+ continue;
301
+ }
302
+ if (seenIds.has(scenario.id)) errors.push(`workflow-scenarios: duplicate id ${scenario.id}`);
303
+ seenIds.add(scenario.id);
304
+ if (!workflows.has(scenario.workflow)) {
305
+ errors.push(`workflow-scenarios: ${file} references unknown workflow ${scenario.workflow}`);
306
+ continue;
307
+ }
308
+ coverage.set(scenario.workflow, (coverage.get(scenario.workflow) ?? 0) + 1);
309
+ const catalogStages = new Set((policy?.selectableStages?.(scenario.workflow)) ?? []);
310
+ for (const stage of scenario.selection?.selectedStages ?? []) {
311
+ if (!primitives.has(stage)) errors.push(`workflow-scenarios: ${file} selects unknown stage ${stage}`);
312
+ else if (catalogStages.size && !catalogStages.has(stage)) errors.push(`workflow-scenarios: ${file} selects non-catalog stage ${stage}`);
313
+ }
314
+ for (const step of scenario.steps ?? []) {
315
+ if (step.stage && !primitives.has(step.stage)) errors.push(`workflow-scenarios: ${file} step references unknown stage ${step.stage}`);
316
+ }
317
+ for (const entry of scenario.expected?.trace ?? []) {
318
+ if (!eventTypes.has(entry.type)) errors.push(`workflow-scenarios: ${file} expects unknown event type ${entry.type}`);
319
+ if (entry.stage !== null && entry.stage !== undefined && !primitives.has(entry.stage)) {
320
+ errors.push(`workflow-scenarios: ${file} expects unknown event stage ${entry.stage}`);
321
+ }
322
+ }
323
+ }
324
+ for (const workflow of WORKFLOW_SKILLS) {
325
+ if (!coverage.get(workflow.id)) errors.push(`workflow-scenarios: ${workflow.id} has no scenario coverage`);
326
+ }
269
327
  return { ok: errors.length === 0, errors };
270
328
  }
271
329
 
@@ -0,0 +1,183 @@
1
+ import {
2
+ validateWorkflowState,
3
+ isWorkflowComplete,
4
+ FORBIDDEN_AUTHORITY_KEYS,
5
+ } from './workflow-state.js';
6
+
7
+ export const WORKFLOW_EVENT_TYPES = Object.freeze([
8
+ 'workflow-created',
9
+ 'stages-selected',
10
+ 'stage-entered',
11
+ 'evidence-recorded',
12
+ 'stage-completed',
13
+ 'stage-skipped',
14
+ 'workflow-blocked',
15
+ 'workflow-interrupted',
16
+ 'workflow-resumed',
17
+ 'workflow-completed',
18
+ ]);
19
+
20
+ const EVENT_TYPE_SET = new Set(WORKFLOW_EVENT_TYPES);
21
+ const EVENT_KEYS = new Set(['seq', 'type', 'workflowId', 'revision', 'stage', 'detail']);
22
+
23
+ function sortedKinds(receipts) {
24
+ return [...receipts].map((r) => r.kind).sort();
25
+ }
26
+
27
+ function sortedQualities(receipts) {
28
+ return [...receipts].map((r) => r.quality).sort();
29
+ }
30
+
31
+ function freezeEvent(event) {
32
+ return Object.freeze({ ...event, detail: Object.freeze({ ...event.detail }) });
33
+ }
34
+
35
+ function makeEvent(seq, type, state, stage, detail) {
36
+ return freezeEvent({
37
+ seq,
38
+ type,
39
+ workflowId: state.workflowId,
40
+ revision: state.revision,
41
+ stage: stage ?? null,
42
+ detail: detail ?? {},
43
+ });
44
+ }
45
+
46
+ export function assertNoAuthority(event) {
47
+ const serialized = JSON.stringify(event).toLowerCase().replace(/[\s_-]+/g, '');
48
+ for (const forbidden of FORBIDDEN_AUTHORITY_KEYS) {
49
+ if (serialized.includes(forbidden)) {
50
+ throw new Error(`workflow trace event must not contain authority-derived content (found ${forbidden})`);
51
+ }
52
+ }
53
+ return true;
54
+ }
55
+
56
+ export function validateWorkflowEvent(event) {
57
+ const errors = [];
58
+ if (event === null || typeof event !== 'object' || Array.isArray(event)) return { ok: false, errors: ['workflow event must be an object'] };
59
+ for (const key of Object.keys(event)) {
60
+ if (!EVENT_KEYS.has(key)) errors.push(`event contains unknown key: ${key}`);
61
+ }
62
+ if (!Number.isInteger(event.seq) || event.seq < 0) errors.push('event seq must be a non-negative integer');
63
+ if (!EVENT_TYPE_SET.has(event.type)) errors.push(`event type must be one of: ${WORKFLOW_EVENT_TYPES.join(', ')}`);
64
+ if (typeof event.workflowId !== 'string' || !event.workflowId) errors.push('event workflowId must be a non-empty string');
65
+ if (!Number.isInteger(event.revision) || event.revision < 0) errors.push('event revision must be a non-negative integer');
66
+ if (event.stage !== null && typeof event.stage !== 'string') errors.push('event stage must be a string or null');
67
+ if (event.detail === null || typeof event.detail !== 'object' || Array.isArray(event.detail)) errors.push('event detail must be an object');
68
+ if (!errors.length) {
69
+ try {
70
+ assertNoAuthority(event);
71
+ } catch (error) {
72
+ errors.push(error.message);
73
+ }
74
+ }
75
+ return { ok: errors.length === 0, errors };
76
+ }
77
+
78
+ export function eventKey(event) {
79
+ return JSON.stringify([event.type, event.workflowId, event.stage, event.detail]);
80
+ }
81
+
82
+ export function normalizeTrace(events) {
83
+ return events.map(eventKey);
84
+ }
85
+
86
+ function requireValidState(state, label) {
87
+ const validation = validateWorkflowState(state);
88
+ if (!validation.ok) throw new Error(`Invalid ${label} workflow state: ${validation.errors.join('; ')}`);
89
+ }
90
+
91
+ export function projectWorkflowEvents(prevState, nextState, input = {}) {
92
+ if (prevState !== null) requireValidState(prevState, 'prev');
93
+ requireValidState(nextState, 'next');
94
+ const op = input.op ?? null;
95
+ if (typeof op !== 'string' || !op) throw new Error('projectWorkflowEvents requires input.op');
96
+ const events = [];
97
+ const emit = (type, stage, detail) => {
98
+ const event = makeEvent(events.length, type, nextState, stage, detail);
99
+ const validation = validateWorkflowEvent(event);
100
+ if (!validation.ok) throw new Error(`Projected invalid event: ${validation.errors.join('; ')}`);
101
+ events.push(event);
102
+ };
103
+ switch (op) {
104
+ case 'create': {
105
+ emit('workflow-created', null, {
106
+ selectedStages: [...nextState.selectedStages],
107
+ candidateStages: [...nextState.candidateStages],
108
+ });
109
+ const sameOrder = nextState.selectedStages.length === nextState.candidateStages.length
110
+ && nextState.selectedStages.every((s, i) => s === nextState.candidateStages[i]);
111
+ if (!sameOrder) emit('stages-selected', null, { selectedStages: [...nextState.selectedStages] });
112
+ break;
113
+ }
114
+ case 'start':
115
+ emit('stage-entered', input.stage ?? nextState.activeStage, {});
116
+ break;
117
+ case 'record': {
118
+ const receipts = input.receipts ?? nextState.evidenceReceipts.slice(-1);
119
+ emit('evidence-recorded', null, { receiptKinds: sortedKinds(receipts), receiptQualities: sortedQualities(receipts) });
120
+ break;
121
+ }
122
+ case 'complete': {
123
+ const entry = nextState.completedStages[nextState.completedStages.length - 1];
124
+ if (!entry) throw new Error('complete projection requires a completed stage entry');
125
+ emit('stage-completed', entry.stage, {
126
+ receiptKinds: sortedKinds(entry.evidenceReceipts),
127
+ receiptQualities: sortedQualities(entry.evidenceReceipts),
128
+ });
129
+ if (nextState.nextStage === null && isWorkflowComplete(nextState)) {
130
+ emit('workflow-completed', null, {
131
+ completedStages: nextState.completedStages.map((c) => c.stage),
132
+ skippedStages: nextState.skippedStages.map((s) => s.stage),
133
+ });
134
+ }
135
+ break;
136
+ }
137
+ case 'skip': {
138
+ const entry = nextState.skippedStages[nextState.skippedStages.length - 1];
139
+ if (!entry) throw new Error('skip projection requires a skipped stage entry');
140
+ emit('stage-skipped', entry.stage, { reason: entry.reason, policy: entry.policy, evidence: [...entry.evidence] });
141
+ break;
142
+ }
143
+ case 'block':
144
+ emit('workflow-blocked', null, {
145
+ blockerIds: [...nextState.blockers].map((b) => b.id).sort(),
146
+ blockerTypes: [...nextState.blockers].map((b) => b.type).sort(),
147
+ });
148
+ break;
149
+ case 'unblock':
150
+ break;
151
+ case 'interrupt':
152
+ emit('workflow-interrupted', null, {});
153
+ break;
154
+ case 'resume': {
155
+ const outcome = input.resolutionOutcome ?? (nextState.status === 'BLOCKED' ? 'blocked' : 'ready');
156
+ emit('workflow-resumed', null, { outcome, staleReason: input.staleReason ?? null });
157
+ if (outcome === 'blocked') {
158
+ emit('workflow-blocked', null, {
159
+ blockerIds: [...nextState.blockers].map((b) => b.id).sort(),
160
+ blockerTypes: [...nextState.blockers].map((b) => b.type).sort(),
161
+ });
162
+ }
163
+ break;
164
+ }
165
+ case 'finalize':
166
+ emit('workflow-completed', null, {
167
+ completedStages: nextState.completedStages.map((c) => c.stage),
168
+ skippedStages: nextState.skippedStages.map((s) => s.stage),
169
+ });
170
+ break;
171
+ default:
172
+ throw new Error(`Unknown workflow trace op: ${op}`);
173
+ }
174
+ return Object.freeze(events);
175
+ }
176
+
177
+ export const workflowTraceAPI = {
178
+ projectWorkflowEvents,
179
+ eventKey,
180
+ normalizeTrace,
181
+ assertNoAuthority,
182
+ validateWorkflowEvent,
183
+ };