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 +31 -0
- package/MIGRATION.md +17 -0
- package/README.md +33 -0
- package/package.json +4 -2
- package/src/validate.js +58 -0
- package/src/workflow-trace.js +183 -0
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.
|
|
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": "
|
|
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
|
+
};
|