musubix3 0.1.19 → 0.1.21
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/.github/plugin/marketplace.json +2 -2
- package/.github/skills/sdd-change/SKILL.md +28 -45
- package/CHANGELOG.md +126 -0
- package/README-ja.md +171 -5
- package/README.md +185 -8
- package/dist/packages/analysis/src/adapters.d.ts +2 -2
- package/dist/packages/analysis/src/adapters.js +33 -3
- package/dist/packages/analysis/src/adapters.js.map +1 -1
- package/dist/packages/analysis/src/approval-record.js +4 -0
- package/dist/packages/analysis/src/approval-record.js.map +1 -1
- package/dist/packages/analysis/src/approval.d.ts +51 -0
- package/dist/packages/analysis/src/approval.js +341 -2
- package/dist/packages/analysis/src/approval.js.map +1 -1
- package/dist/packages/analysis/src/attestation.js +4 -0
- package/dist/packages/analysis/src/attestation.js.map +1 -1
- package/dist/packages/analysis/src/change-evidence.d.ts +65 -8
- package/dist/packages/analysis/src/change-evidence.js +173 -46
- package/dist/packages/analysis/src/change-evidence.js.map +1 -1
- package/dist/packages/analysis/src/change-waiver.d.ts +46 -13
- package/dist/packages/analysis/src/change-waiver.js +481 -152
- package/dist/packages/analysis/src/change-waiver.js.map +1 -1
- package/dist/packages/analysis/src/change.d.ts +1 -0
- package/dist/packages/analysis/src/change.js +194 -66
- package/dist/packages/analysis/src/change.js.map +1 -1
- package/dist/packages/analysis/src/evidence-merge-guard.d.ts +9 -0
- package/dist/packages/analysis/src/evidence-merge-guard.js +62 -0
- package/dist/packages/analysis/src/evidence-merge-guard.js.map +1 -0
- package/dist/packages/analysis/src/evidence-merge.d.ts +55 -0
- package/dist/packages/analysis/src/evidence-merge.js +1182 -0
- package/dist/packages/analysis/src/evidence-merge.js.map +1 -0
- package/dist/packages/analysis/src/evidence-writer-lock.d.ts +189 -0
- package/dist/packages/analysis/src/evidence-writer-lock.js +843 -0
- package/dist/packages/analysis/src/evidence-writer-lock.js.map +1 -0
- package/dist/packages/analysis/src/files.js +11 -0
- package/dist/packages/analysis/src/files.js.map +1 -1
- package/dist/packages/analysis/src/formal.js +7 -0
- package/dist/packages/analysis/src/formal.js.map +1 -1
- package/dist/packages/analysis/src/gate.d.ts +1 -1
- package/dist/packages/analysis/src/gate.js +23 -6
- package/dist/packages/analysis/src/gate.js.map +1 -1
- package/dist/packages/analysis/src/graph.js +6 -0
- package/dist/packages/analysis/src/graph.js.map +1 -1
- package/dist/packages/analysis/src/index.d.ts +3 -0
- package/dist/packages/analysis/src/index.js +3 -0
- package/dist/packages/analysis/src/index.js.map +1 -1
- package/dist/packages/analysis/src/knowledge.js +4 -0
- package/dist/packages/analysis/src/knowledge.js.map +1 -1
- package/dist/packages/analysis/src/model-correspondence.js +5 -1
- package/dist/packages/analysis/src/model-correspondence.js.map +1 -1
- package/dist/packages/analysis/src/mutation.js +4 -0
- package/dist/packages/analysis/src/mutation.js.map +1 -1
- package/dist/packages/analysis/src/order.d.ts +16 -1
- package/dist/packages/analysis/src/order.js +10 -0
- package/dist/packages/analysis/src/order.js.map +1 -1
- package/dist/packages/analysis/src/performance.js +5 -1
- package/dist/packages/analysis/src/performance.js.map +1 -1
- package/dist/packages/analysis/src/quality-refresh.d.ts +18 -0
- package/dist/packages/analysis/src/quality-refresh.js +312 -0
- package/dist/packages/analysis/src/quality-refresh.js.map +1 -0
- package/dist/packages/analysis/src/tdd.d.ts +86 -3
- package/dist/packages/analysis/src/tdd.js +604 -52
- package/dist/packages/analysis/src/tdd.js.map +1 -1
- package/dist/packages/analysis/src/trace.d.ts +24 -0
- package/dist/packages/analysis/src/trace.js +101 -3
- package/dist/packages/analysis/src/trace.js.map +1 -1
- package/dist/packages/analysis/src/workflow-waiver.d.ts +6 -2
- package/dist/packages/analysis/src/workflow-waiver.js +9 -5
- package/dist/packages/analysis/src/workflow-waiver.js.map +1 -1
- package/dist/packages/analysis/src/workflow.d.ts +1 -1
- package/dist/packages/analysis/src/workflow.js +103 -21
- package/dist/packages/analysis/src/workflow.js.map +1 -1
- package/dist/packages/cli/src/install.js +51 -6
- package/dist/packages/cli/src/install.js.map +1 -1
- package/dist/packages/cli/src/main.js +252 -79
- package/dist/packages/cli/src/main.js.map +1 -1
- package/dist/packages/domain/src/design.js +8 -1
- package/dist/packages/domain/src/design.js.map +1 -1
- package/dist/packages/domain/src/types.d.ts +1 -0
- package/package.json +7 -2
- package/plugin.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command, CommanderError, InvalidArgumentError } from 'commander';
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
3
4
|
import { basename, relative, resolve } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
6
|
import { c4Diagram, validateConstitution, validateDesign, validateRequirements, } from '../../domain/src/index.js';
|
|
6
|
-
import { buildKnowledge, buildTrace, changedFiles, checkTrace, configLint, cycles, exists, files, formalCheck, graphGate, graphImpact, indexGraph, loadConfig, loadGraph, loadTrace, portable, projectStatus, queryKnowledge, formalDoctor, generateFormalArtifacts, readText, runGate, traceImpact, changePhases, recordChangePhase, recordWorkflow, runTddPhase, sanitizeWorkflowLogFile, validateTddEvidence, verifyWorkflowLogFile, migrateTddFingerprint, voidTddCycle, attestationSigningPayload, createUnsignedAttestation, githubOidcAudience, verifyEvidenceAttestation, mutationDoctor, mutationIdentity, validateMutationEvidence, validateModelCorrespondenceEvidence, within, approvalManifest, approvalStages, recordApproval, requireApproval, requireDomainOption, requireValidateDomainOption, resolveDesignFileDomain, resolveNamedDomain, validateApprovals, validateApprovalsForDomain, scaffoldCommands, scaffoldRequirements, scaffoldDesign, recordChangeWaiver, recordWorkflowWaiver, recordAllWorkflowWaivers, } from '../../analysis/src/index.js';
|
|
7
|
+
import { buildKnowledge, buildTrace, changedFiles, checkTrace, configLint, cycles, exists, files, formalCheck, graphGate, graphImpact, indexGraph, loadConfig, loadGraph, loadTrace, portable, projectStatus, queryKnowledge, formalDoctor, generateFormalArtifacts, readText, runGate, traceImpact, changePhases, recordChangePhase, recordWorkflow, runTddPhase, sanitizeWorkflowLogFile, validateTddEvidence, verifyWorkflowLogFile, migrateTddFingerprint, migrateTddIdentifier, voidTddCycle, archiveTddCycle, mergeEvidenceHistories, recoverEvidenceMerge, recoverQualityRefresh, attestationSigningPayload, createUnsignedAttestation, githubOidcAudience, verifyEvidenceAttestation, mutationDoctor, mutationIdentity, validateMutationEvidence, validateModelCorrespondenceEvidence, within, approvalManifest, approvalStages, diffOnlyChangedFiles, loadApproval, recordApproval, requireApproval, requireDomainOption, requireValidateDomainOption, resolveDesignFileDomain, resolveNamedDomain, validateApprovals, validateApprovalsForDomain, scaffoldCommands, scaffoldRequirements, scaffoldDesign, recordChangeWaiver, recordWorkflowWaiver, recordAllWorkflowWaivers, assertCoordinatedEvidenceRead, EvidenceProtectedOutputError, EvidenceWriterLockError, recoverEvidenceWriterLock, withEvidenceWriterLock, } from '../../analysis/src/index.js';
|
|
7
8
|
import { install, pluginInstall, upgradeSkills } from './install.js';
|
|
8
9
|
const packageRoot = fileURLToPath(new URL('../../../../', import.meta.url));
|
|
9
10
|
function output(value, json, summary) {
|
|
@@ -23,8 +24,12 @@ function common(command) {
|
|
|
23
24
|
function pathQuery(root, query) {
|
|
24
25
|
return portable(relative(root, resolve(root, query)));
|
|
25
26
|
}
|
|
27
|
+
async function coordinatedReader(root, operation) {
|
|
28
|
+
await assertCoordinatedEvidenceRead(root);
|
|
29
|
+
return operation();
|
|
30
|
+
}
|
|
26
31
|
export function createProgram() {
|
|
27
|
-
const program = new Command().name('musubix3').description('Evidence-driven SDD for GitHub Copilot CLI / 根拠に基づく仕様駆動開発').version('0.1.
|
|
32
|
+
const program = new Command().name('musubix3').description('Evidence-driven SDD for GitHub Copilot CLI / 根拠に基づく仕様駆動開発').version('0.1.21');
|
|
28
33
|
program.exitOverride();
|
|
29
34
|
common(program.command('init').alias('install').description('Install repository skills and SDD artifacts (preserves existing files)'))
|
|
30
35
|
.option('--dry-run', 'Preview without writing').option('--force', 'Replace bundled, managed paths only')
|
|
@@ -85,27 +90,31 @@ export function createProgram() {
|
|
|
85
90
|
}
|
|
86
91
|
common(design.command('validate <file>')).action(async (file, options) => {
|
|
87
92
|
const root = resolve(options.root);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
await coordinatedReader(root, async () => {
|
|
94
|
+
if (await exists(within(root, '.musubix/config.json'))) {
|
|
95
|
+
const config = await loadConfig(root);
|
|
96
|
+
const domain = await resolveDesignFileDomain(root, config.approval, portable(relative(root, resolve(root, file))));
|
|
97
|
+
await requireApproval(root, 'requirements', config.approval, domain);
|
|
98
|
+
}
|
|
99
|
+
result(await designResult(file, root), !!options.json);
|
|
100
|
+
});
|
|
94
101
|
});
|
|
95
102
|
common(design.command('c4 <file>')).action(async (file, options) => {
|
|
96
103
|
const root = resolve(options.root);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
await coordinatedReader(root, async () => {
|
|
105
|
+
if (await exists(within(root, '.musubix/config.json'))) {
|
|
106
|
+
const config = await loadConfig(root);
|
|
107
|
+
const domain = await resolveDesignFileDomain(root, config.approval, portable(relative(root, resolve(root, file))));
|
|
108
|
+
await requireApproval(root, 'requirements', config.approval, domain);
|
|
109
|
+
}
|
|
110
|
+
const report = await designResult(file, root);
|
|
111
|
+
if (!report.valid) {
|
|
112
|
+
result(report, !!options.json);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const diagram = c4Diagram(report.value);
|
|
116
|
+
output({ diagram }, !!options.json, diagram);
|
|
117
|
+
});
|
|
109
118
|
});
|
|
110
119
|
common(design.command('scaffold <slug>')).action(async (slug, options) => {
|
|
111
120
|
const path = await scaffoldDesign(resolve(options.root), slug);
|
|
@@ -121,10 +130,12 @@ export function createProgram() {
|
|
|
121
130
|
common(trace.command('check')).option('--strict', 'Fail missing mandatory coverage')
|
|
122
131
|
.action(async (options) => {
|
|
123
132
|
const root = resolve(options.root);
|
|
133
|
+
await assertCoordinatedEvidenceRead(root);
|
|
124
134
|
result(await checkTrace(root, await loadTrace(root), !!options.strict), !!options.json);
|
|
125
135
|
});
|
|
126
136
|
common(trace.command('impact <id-or-path>')).action(async (query, options) => {
|
|
127
137
|
const root = resolve(options.root);
|
|
138
|
+
await assertCoordinatedEvidenceRead(root);
|
|
128
139
|
const graph = await loadTrace(root);
|
|
129
140
|
const freshness = await checkTrace(root, graph);
|
|
130
141
|
const stale = freshness.diagnostics.some((d) => d.code.startsWith('TRACE_STALE'));
|
|
@@ -144,28 +155,35 @@ export function createProgram() {
|
|
|
144
155
|
common(graph.command('index')).option('--changed', 'Report changed files; conservatively refresh full graph')
|
|
145
156
|
.action(async (options) => {
|
|
146
157
|
const root = resolve(options.root);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
158
|
+
await withEvidenceWriterLock(root, 'graph index', async () => {
|
|
159
|
+
const changed = options.changed ? await changedFiles(root) : null;
|
|
160
|
+
const indexed = await indexGraph(root);
|
|
161
|
+
output({ ...indexed, changed }, !!options.json, `Graph: ${indexed.files.length} files, ${indexed.imports.length} imports, ${indexed.symbols.length} symbols.`);
|
|
162
|
+
if (indexed.diagnostics.some((d) => d.severity === 'error'))
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
});
|
|
152
165
|
});
|
|
153
166
|
common(graph.command('impact <symbol-or-path>')).action(async (query, options) => {
|
|
154
167
|
const root = resolve(options.root);
|
|
168
|
+
await assertCoordinatedEvidenceRead(root);
|
|
155
169
|
const indexed = await loadGraph(root);
|
|
156
170
|
const normalized = indexed.files.includes(pathQuery(root, query)) ? pathQuery(root, query) : query;
|
|
157
171
|
output(graphImpact(indexed, normalized), !!options.json);
|
|
158
172
|
});
|
|
159
173
|
common(graph.command('cycles')).action(async (options) => {
|
|
160
|
-
const
|
|
174
|
+
const root = resolve(options.root);
|
|
175
|
+
await assertCoordinatedEvidenceRead(root);
|
|
176
|
+
const found = cycles(await loadGraph(root));
|
|
161
177
|
output({ cycles: found }, !!options.json);
|
|
162
178
|
if (found.length)
|
|
163
179
|
process.exitCode = 1;
|
|
164
180
|
});
|
|
165
181
|
common(graph.command('gate')).action(async (options) => {
|
|
166
182
|
const root = resolve(options.root);
|
|
167
|
-
|
|
168
|
-
|
|
183
|
+
await withEvidenceWriterLock(root, 'graph gate', async () => {
|
|
184
|
+
const config = await loadConfig(root);
|
|
185
|
+
result(graphGate(await indexGraph(root), config.architecture, config.codeGraph), !!options.json);
|
|
186
|
+
});
|
|
169
187
|
});
|
|
170
188
|
const knowledge = program.command('knowledge').description('Local artifact and Git evidence retrieval (TF-IDF, not GraphRAG)');
|
|
171
189
|
common(knowledge.command('build')).action(async (options) => {
|
|
@@ -177,7 +195,9 @@ export function createProgram() {
|
|
|
177
195
|
const limit = Number(options.limit);
|
|
178
196
|
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
179
197
|
throw new Error('--limit must be 1..100.');
|
|
180
|
-
|
|
198
|
+
const root = resolve(options.root);
|
|
199
|
+
await assertCoordinatedEvidenceRead(root);
|
|
200
|
+
output(await queryKnowledge(root, query.join(' '), limit), !!options.json);
|
|
181
201
|
});
|
|
182
202
|
const formal = program.command('formal').description('Honest consistency checking of an explicit abstraction');
|
|
183
203
|
common(formal.command('check <file>'))
|
|
@@ -192,12 +212,12 @@ export function createProgram() {
|
|
|
192
212
|
if (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 300_000)
|
|
193
213
|
throw new Error('--timeout must be 100..300000 milliseconds.');
|
|
194
214
|
const root = resolve(options.root);
|
|
195
|
-
const report = await formalCheck(await readText(root, file), root, {
|
|
215
|
+
const report = await withEvidenceWriterLock(root, 'formal check', async () => formalCheck(await readText(root, file), root, {
|
|
196
216
|
solver: options.solver,
|
|
197
217
|
timeoutMs,
|
|
198
218
|
...(options.z3Command ? { z3Command: options.z3Command } : {}),
|
|
199
219
|
...(options.leanCommand ? { leanCommand: options.leanCommand } : {}),
|
|
200
|
-
});
|
|
220
|
+
}));
|
|
201
221
|
output(report, !!options.json);
|
|
202
222
|
if (!report.valid)
|
|
203
223
|
process.exitCode = 1;
|
|
@@ -209,7 +229,7 @@ export function createProgram() {
|
|
|
209
229
|
throw new Error('Unknown format; use both, smt2, or lean.');
|
|
210
230
|
const root = resolve(options.root);
|
|
211
231
|
const formats = options.format === 'both' ? ['smt2', 'lean'] : [options.format];
|
|
212
|
-
const report = await generateFormalArtifacts(await readText(root, file), root, [...formats]);
|
|
232
|
+
const report = await withEvidenceWriterLock(root, 'formal generate', async () => generateFormalArtifacts(await readText(root, file), root, [...formats]));
|
|
213
233
|
output(report, !!options.json, report.artifacts.map((entry) => `${entry.format}: ${entry.path}`).join('\n'));
|
|
214
234
|
if (!report.valid)
|
|
215
235
|
process.exitCode = 1;
|
|
@@ -258,6 +278,61 @@ export function createProgram() {
|
|
|
258
278
|
common(evidence.command('refresh'))
|
|
259
279
|
.option('--changed', 'Preserve changed-file impact context while refreshing all checks')
|
|
260
280
|
.action(executeGate);
|
|
281
|
+
common(evidence.command('unlock')
|
|
282
|
+
.description('Inspect and recover an abandoned project evidence-writer lock')
|
|
283
|
+
.requiredOption('--recover', 'Recover only a demonstrably dead same-host owner')
|
|
284
|
+
.addHelpText('after', `
|
|
285
|
+
Recovery never steals a live, remote, malformed, PID-reused, or indeterminate
|
|
286
|
+
lock. Inspect the reported owner and exact lock path before any targeted manual
|
|
287
|
+
removal. If a merge journal is also pending, recover this writer lock first,
|
|
288
|
+
then run evidence merge --recover.
|
|
289
|
+
|
|
290
|
+
Windows directory synchronization may be unavailable after atomic publication
|
|
291
|
+
or verified release. Automatic recovery remains inspection-only on Windows and
|
|
292
|
+
macOS.`))
|
|
293
|
+
.action(async (options) => {
|
|
294
|
+
if (!options.recover)
|
|
295
|
+
throw new Error('--recover is required.');
|
|
296
|
+
const report = await recoverEvidenceWriterLock(resolve(options.root));
|
|
297
|
+
output(report, !!options.json, report.action);
|
|
298
|
+
});
|
|
299
|
+
common(evidence.command('merge')
|
|
300
|
+
.description('Deterministically merge another valid append-only evidence history into the current root')
|
|
301
|
+
.option('--incoming <directory>', 'Project directory containing the incoming .musubix/evidence history')
|
|
302
|
+
.option('--dry-run', 'Validate and preview the merge without writing evidence')
|
|
303
|
+
.option('--recover', 'Recover an interrupted merge without requiring --incoming')
|
|
304
|
+
.addHelpText('after', `
|
|
305
|
+
Both roots must contain individually valid order, TDD, and change evidence. The
|
|
306
|
+
incoming root must be path-disjoint and is never modified. Merge uses canonical
|
|
307
|
+
duplicate comparison and stable base-first ordering; recordedAt warnings may
|
|
308
|
+
remain. The current worktree must already contain incoming change documents.
|
|
309
|
+
Merge only handles order.json, tdd.json, changes.json, and change-waivers.json.
|
|
310
|
+
Run gate/status afterward and explicitly re-approve stale waivers. If base
|
|
311
|
+
Quality precedes incoming batch evidence, merge before recording Quality.
|
|
312
|
+
Use --recover for interrupted transactions; unsafe recovery requires backing up
|
|
313
|
+
the evidence directory, restoring/verifying all four targets from a trusted
|
|
314
|
+
source, quarantining merge files, and rerunning structural validation.`))
|
|
315
|
+
.action(async (options) => {
|
|
316
|
+
const root = resolve(options.root);
|
|
317
|
+
if (options.recover) {
|
|
318
|
+
if (options.incoming || options.dryRun)
|
|
319
|
+
throw new Error('--recover cannot be combined with --incoming or --dry-run.');
|
|
320
|
+
const report = await recoverEvidenceMerge(root);
|
|
321
|
+
output(report, !!options.json, report.action);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!options.incoming)
|
|
325
|
+
throw new Error('--incoming <directory> is required unless --recover is used.');
|
|
326
|
+
const report = await mergeEvidenceHistories(root, resolve(options.incoming), { ...(options.dryRun ? { dryRun: true } : {}) });
|
|
327
|
+
output(report, !!options.json, `${report.valid ? (options.dryRun ? 'DRY-RUN' : 'MERGED') : 'FAILED'}`
|
|
328
|
+
+ ` preserved=${report.preserved} deduplicated=${report.deduplicated} appended=${report.appended}`
|
|
329
|
+
+ `${report.diagnostics.length ? `\n${report.diagnostics.map((item) => `${item.code}${item.file ? ` ${item.file}` : ''}${item.identity ? ` ${item.identity}` : ''}: ${item.message}`).join('\n')}` : ''}`
|
|
330
|
+
+ `${report.followUpDiagnostics.length ? `\n${report.followUpDiagnostics.map((item) => `${item.code}${item.path ? ` ${item.path}` : ''}: ${item.message}`).join('\n')}` : ''}`
|
|
331
|
+
+ `${report.supersededScopes.length ? `\nWaiver supersession changed for ${report.supersededScopes.length} scope(s).` : ''}`
|
|
332
|
+
+ `${report.revalidationRequired ? '\nEVIDENCE_MERGE_REVALIDATION_REQUIRED: rerun gate and status; re-approve stale waivers.' : ''}`);
|
|
333
|
+
if (!report.valid)
|
|
334
|
+
process.exitCode = 1;
|
|
335
|
+
});
|
|
261
336
|
const mutation = program.command('mutation').description('Inspect and validate requirement-scoped mutation evidence');
|
|
262
337
|
common(mutation.command('doctor')).action(async (options) => {
|
|
263
338
|
const report = await mutationDoctor(resolve(options.root));
|
|
@@ -268,7 +343,9 @@ export function createProgram() {
|
|
|
268
343
|
: 'No supported project ecosystem was detected.');
|
|
269
344
|
});
|
|
270
345
|
common(mutation.command('validate')).action(async (options) => {
|
|
271
|
-
const
|
|
346
|
+
const root = resolve(options.root);
|
|
347
|
+
await assertCoordinatedEvidenceRead(root);
|
|
348
|
+
const report = await validateMutationEvidence(root);
|
|
272
349
|
if (!options.json && !report.present) {
|
|
273
350
|
console.log('No mutation evidence at .musubix/evidence/mutation.json; the gate converts a configured mutationReport into that file. Compatible mode does not require it.');
|
|
274
351
|
}
|
|
@@ -289,7 +366,9 @@ export function createProgram() {
|
|
|
289
366
|
common(correspondence.command('validate')
|
|
290
367
|
/* @id CODE-MODEL-CORRESPONDENCE-EVIDENCE-GUIDANCE-002 */
|
|
291
368
|
.description('Validate model correspondence evidence (run `npx musubix3 evidence refresh` first to generate .musubix/evidence/model-correspondence.json)')).action(async (options) => {
|
|
292
|
-
const
|
|
369
|
+
const root = resolve(options.root);
|
|
370
|
+
await assertCoordinatedEvidenceRead(root);
|
|
371
|
+
const report = await validateModelCorrespondenceEvidence(root);
|
|
293
372
|
result(report, !!options.json);
|
|
294
373
|
});
|
|
295
374
|
common(program.command('workflow-record <skill> <phase>').description('Record a self-reported workflow declaration'))
|
|
@@ -313,28 +392,30 @@ export function createProgram() {
|
|
|
313
392
|
.option('--session-id <uuid>', 'Require the terminal result to identify this Copilot session')
|
|
314
393
|
.action(async (logs, options) => {
|
|
315
394
|
const root = resolve(options.root);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
395
|
+
await withEvidenceWriterLock(root, 'workflow-verify', async () => {
|
|
396
|
+
const paths = logs.map((log) => resolve(log));
|
|
397
|
+
for (const path of paths) {
|
|
398
|
+
const info = await stat(path);
|
|
399
|
+
if (!info.isFile())
|
|
400
|
+
throw new Error('Workflow log must be a file.');
|
|
401
|
+
}
|
|
402
|
+
const configured = (await loadConfig(root)).workflow;
|
|
403
|
+
const mode = options.strict || options.sessionId ? 'strict' : configured.mode;
|
|
404
|
+
const expectedSessionId = options.sessionId ?? configured.expectedSessionId;
|
|
405
|
+
const manifest = await verifyWorkflowLogFile(root, paths.length === 1 ? paths[0] : paths, {
|
|
406
|
+
mode,
|
|
407
|
+
...(expectedSessionId ? { expectedSessionId } : {}),
|
|
408
|
+
...(configured.maxAgeSeconds === undefined ? {} : { maxAgeSeconds: configured.maxAgeSeconds }),
|
|
409
|
+
...(configured.maxFutureSkewSeconds === undefined
|
|
410
|
+
? {}
|
|
411
|
+
: { maxFutureSkewSeconds: configured.maxFutureSkewSeconds }),
|
|
412
|
+
...(configured.maxEventSkewMs === undefined ? {} : { maxEventSkewMs: configured.maxEventSkewMs }),
|
|
413
|
+
...(configured.maxTranscriptBytes === undefined ? {} : { maxBytes: configured.maxTranscriptBytes }),
|
|
414
|
+
...(configured.maxTranscriptLineBytes === undefined ? {} : { maxLineBytes: configured.maxTranscriptLineBytes }),
|
|
415
|
+
});
|
|
416
|
+
output(manifest, !!options.json, `Verified ${manifest.verification?.invocations.length ?? 0} Copilot Skill invocation event(s)`
|
|
417
|
+
+ `${manifest.verification?.sessionId ? ` in session ${manifest.verification.sessionId}` : ''}.`);
|
|
335
418
|
});
|
|
336
|
-
output(manifest, !!options.json, `Verified ${manifest.verification?.invocations.length ?? 0} Copilot Skill invocation event(s)`
|
|
337
|
-
+ `${manifest.verification?.sessionId ? ` in session ${manifest.verification.sessionId}` : ''}.`);
|
|
338
419
|
});
|
|
339
420
|
common(program.command('workflow-sanitize <log> <output-file>').description('Write a privacy-minimized Skill lifecycle transcript'))
|
|
340
421
|
.option('--session-id <uuid>', 'Replace the terminal session ID with a review-safe UUID')
|
|
@@ -416,6 +497,7 @@ export function createProgram() {
|
|
|
416
497
|
if (!['github', 'azure-pipelines', 'generic'].includes(options.provider))
|
|
417
498
|
throw new Error('Unknown attestation provider.');
|
|
418
499
|
const root = resolve(options.root);
|
|
500
|
+
await assertCoordinatedEvidenceRead(root);
|
|
419
501
|
const configured = (await loadConfig(root)).attestation.githubOidc;
|
|
420
502
|
if (configured?.mode === 'strict' && options.provider !== 'github') {
|
|
421
503
|
throw new Error('Strict GitHub OIDC attestation requires --provider github.');
|
|
@@ -448,6 +530,7 @@ export function createProgram() {
|
|
|
448
530
|
});
|
|
449
531
|
common(attestation.command('verify')).action(async (options) => {
|
|
450
532
|
const root = resolve(options.root);
|
|
533
|
+
await assertCoordinatedEvidenceRead(root);
|
|
451
534
|
const report = await verifyEvidenceAttestation(root, (await loadConfig(root)).attestation);
|
|
452
535
|
output(report, !!options.json, `${report.status}: ${report.valid ? 'valid' : 'invalid'}`);
|
|
453
536
|
if (!report.valid)
|
|
@@ -481,6 +564,16 @@ export function createProgram() {
|
|
|
481
564
|
output(evidence, !!options.json, options.dryRun ? `Would record ${changeId}:${phase}.` : `Recorded ${changeId}:${phase}.`);
|
|
482
565
|
});
|
|
483
566
|
const change = program.command('change').description('Change chronology and waiver evidence');
|
|
567
|
+
/** @id CODE-CHANGE-QUALITY-REFRESH-005
|
|
568
|
+
* @implements REQ-CHANGE-QUALITY-REFRESH-003
|
|
569
|
+
* @design DES-CHANGE-QUALITY-REFRESH-002
|
|
570
|
+
*/
|
|
571
|
+
common(change.command('quality-recover')
|
|
572
|
+
.description('Recover an interrupted two-file Quality refresh transaction'))
|
|
573
|
+
.action(async (options) => {
|
|
574
|
+
const report = await recoverQualityRefresh(resolve(options.root));
|
|
575
|
+
output(report, !!options.json, report.recovered ? `Quality refresh ${report.action}.` : 'No Quality refresh to recover.');
|
|
576
|
+
});
|
|
484
577
|
const waiver = change.command('waiver').description('Record an audited, bounded downgrade of a recording-order-debt diagnostic');
|
|
485
578
|
common(waiver.command('record <change-id> <code>')
|
|
486
579
|
.description('Downgrade one currently-present waivable diagnostic from error to warning, as an appended, hash-chained record'))
|
|
@@ -496,17 +589,41 @@ export function createProgram() {
|
|
|
496
589
|
output(result, !!options.json, `WAIVER: PASS (${changeId}:${code}${options.requirement ? `:${options.requirement}` : ''}${options.detail ? `:${options.detail}` : ''})`);
|
|
497
590
|
});
|
|
498
591
|
const approval = program.command('approval').description('Prepare, record and validate explicit artifact-bound human approvals');
|
|
592
|
+
/** @id CODE-APPROVAL-PREPARE-DIFF-ONLY-003
|
|
593
|
+
* @implements REQ-APPROVAL-PREPARE-DIFF-ONLY-001 REQ-APPROVAL-PREPARE-DIFF-ONLY-002 REQ-APPROVAL-PREPARE-DIFF-ONLY-004
|
|
594
|
+
* @implements REQ-APPROVAL-PREPARE-DIFF-ONLY-005 REQ-APPROVAL-PREPARE-DIFF-ONLY-006 REQ-APPROVAL-PREPARE-DIFF-ONLY-007
|
|
595
|
+
* @design DES-APPROVAL-PREPARE-DIFF-ONLY-002 DES-APPROVAL-PREPARE-DIFF-ONLY-003
|
|
596
|
+
*/
|
|
499
597
|
common(approval.command('prepare <stage>').description('Show the exact artifact manifest a human must review'))
|
|
500
598
|
.option('--domain <name>', 'Approval domain (required when approval.domains is configured, except for release)')
|
|
599
|
+
.option('--diff-only', 'Additionally report only the file paths whose content changed since the last recorded approval for this stage')
|
|
501
600
|
.action(async (stage, options) => {
|
|
502
601
|
if (!approvalStages.includes(stage))
|
|
503
602
|
throw new Error(`stage must be one of: ${approvalStages.join(', ')}`);
|
|
504
603
|
const root = resolve(options.root);
|
|
604
|
+
await assertCoordinatedEvidenceRead(root);
|
|
505
605
|
const config = await loadConfig(root);
|
|
506
606
|
requireDomainOption(config.approval, stage, options.domain);
|
|
507
607
|
const domain = options.domain ? await resolveNamedDomain(root, config.approval, options.domain) : undefined;
|
|
508
608
|
const manifest = await approvalManifest(root, stage, domain);
|
|
509
|
-
|
|
609
|
+
if (!options.diffOnly) {
|
|
610
|
+
output(manifest, !!options.json, `${stage} artifact manifest: ${manifest.artifactSha256}\n${Object.keys(manifest.artifacts).join('\n')}`);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
// REQ-APPROVAL-PREPARE-DIFF-ONLY-007: no try/catch here — a corrupted prior
|
|
614
|
+
// approval file must abort the command via loadApproval's thrown error,
|
|
615
|
+
// never be silently treated as "no prior approval".
|
|
616
|
+
const previous = await loadApproval(root, stage, domain?.name);
|
|
617
|
+
const { changedFiles: changed, diffOnlyBaseline } = diffOnlyChangedFiles(manifest, previous);
|
|
618
|
+
const withDiff = { ...manifest, changedFiles: changed, diffOnlyBaseline };
|
|
619
|
+
// REQ-APPROVAL-PREPARE-DIFF-ONLY-006: the non-JSON summary shows only the
|
|
620
|
+
// changed paths (plus the full hash and an explicit baseline note), never
|
|
621
|
+
// the complete artifact listing; --json output above is unaffected.
|
|
622
|
+
const baselineNote = diffOnlyBaseline === 'none'
|
|
623
|
+
? 'no prior approval found; showing all current artifacts'
|
|
624
|
+
: 'compared against the previously recorded approval';
|
|
625
|
+
const diffSummary = `${stage} artifact manifest: ${manifest.artifactSha256}\n(diff-only: ${baselineNote})\n${changed.join('\n')}`;
|
|
626
|
+
output(withDiff, !!options.json, diffSummary);
|
|
510
627
|
});
|
|
511
628
|
common(approval.command('record <stage>'))
|
|
512
629
|
.requiredOption('--approver <name>', 'Human approver name')
|
|
@@ -520,14 +637,17 @@ export function createProgram() {
|
|
|
520
637
|
if (options.confirm !== true)
|
|
521
638
|
throw new Error('--confirm is required to record human approval.');
|
|
522
639
|
const root = resolve(options.root);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
640
|
+
await withEvidenceWriterLock(root, 'approval record', async () => {
|
|
641
|
+
const config = await loadConfig(root);
|
|
642
|
+
const evidence = await recordApproval(root, stage, options.approver, options.artifactSha256, config.approval, options.domain);
|
|
643
|
+
output(evidence, !!options.json, `Recorded explicit ${stage} approval by ${evidence.approver} for ${evidence.artifactSha256}.`);
|
|
644
|
+
});
|
|
526
645
|
});
|
|
527
646
|
common(approval.command('validate'))
|
|
528
647
|
.option('--domain <name>', 'Limit the report to one approval domain')
|
|
529
648
|
.action(async (options) => {
|
|
530
649
|
const root = resolve(options.root);
|
|
650
|
+
await assertCoordinatedEvidenceRead(root);
|
|
531
651
|
const config = await loadConfig(root);
|
|
532
652
|
requireValidateDomainOption(config.approval, options.domain);
|
|
533
653
|
const report = options.domain
|
|
@@ -544,7 +664,9 @@ export function createProgram() {
|
|
|
544
664
|
+ "name suffix for go-test/cargo, an additional @Tag for junit, DisplayName for dotnet). "
|
|
545
665
|
+ 'See README.md "Adapter test-ID declaration reference" for the full table and worked examples.');
|
|
546
666
|
common(tdd.command('validate')).action(async (options) => {
|
|
547
|
-
const
|
|
667
|
+
const root = resolve(options.root);
|
|
668
|
+
await assertCoordinatedEvidenceRead(root);
|
|
669
|
+
const report = await validateTddEvidence(root);
|
|
548
670
|
result(report, !!options.json);
|
|
549
671
|
});
|
|
550
672
|
for (const phase of ['red', 'green', 'refactor']) {
|
|
@@ -559,13 +681,14 @@ export function createProgram() {
|
|
|
559
681
|
// onto `green`/`refactor` via the shared loop.
|
|
560
682
|
if (phase === 'red') {
|
|
561
683
|
phaseCommand.description('Record a failing (Red) test as TDD evidence for a requirement. Persisting the '
|
|
562
|
-
+ "project's first cycle here
|
|
563
|
-
+ 'every mandatory requirement (each uncovered one surfaced as '
|
|
564
|
-
+ 'TDD_REQUIREMENT_UNCOVERED);
|
|
565
|
-
+ '
|
|
566
|
-
+ '
|
|
567
|
-
+ '
|
|
568
|
-
+ '
|
|
684
|
+
+ "project's first cycle here activates gate's tdd coverage evaluation project-wide "
|
|
685
|
+
+ 'for every mandatory requirement (each uncovered one surfaced as '
|
|
686
|
+
+ 'TDD_REQUIREMENT_UNCOVERED); if tdd was not already required for another '
|
|
687
|
+
+ 'configured reason, that same first cycle is what makes the check required. '
|
|
688
|
+
+ '"approval record release" always runs the full (non-\'--changed\') gate, so '
|
|
689
|
+
+ 'it is blocked by any resulting TDD_REQUIREMENT_UNCOVERED diagnostics. '
|
|
690
|
+
+ '"tdd migrate" cannot bulk-onboard previously-uncovered requirements: it only '
|
|
691
|
+
+ 'reuses already-covered valid Green-backed evidence.');
|
|
569
692
|
}
|
|
570
693
|
phaseCommand
|
|
571
694
|
.requiredOption('--requirement <id>', 'Requirement ID verified by the test')
|
|
@@ -580,17 +703,23 @@ export function createProgram() {
|
|
|
580
703
|
process.exitCode = 1;
|
|
581
704
|
});
|
|
582
705
|
}
|
|
583
|
-
common(tdd.command('migrate
|
|
584
|
-
.
|
|
706
|
+
common(tdd.command('migrate [ids...]'))
|
|
707
|
+
.description('Relink existing covered evidence: `tdd migrate <test-id>` re-fingerprints one covered test; `tdd migrate <old-id> <new-id>` relinks a pure identifier rename.')
|
|
708
|
+
.requiredOption('--approver <name>', 'Human approver recording this migrate operation')
|
|
585
709
|
.option('--confirm', 'Confirm the migration is reviewed and intended', false)
|
|
586
|
-
.action(async (
|
|
710
|
+
.action(async (ids, options) => {
|
|
587
711
|
if (!options.confirm)
|
|
588
|
-
throw new Error('
|
|
712
|
+
throw new Error('TDD migration requires --confirm.');
|
|
713
|
+
if (ids.length < 1 || ids.length > 2)
|
|
714
|
+
throw new InvalidArgumentError('tdd migrate accepts either <test-id> or <old-id> <new-id>.');
|
|
589
715
|
const root = resolve(options.root);
|
|
590
|
-
const migration =
|
|
716
|
+
const migration = ids.length === 1
|
|
717
|
+
? await migrateTddFingerprint(root, ids[0], options.approver)
|
|
718
|
+
: await migrateTddIdentifier(root, ids[0], ids[1], options.approver);
|
|
719
|
+
const target = ids.join(' -> ');
|
|
591
720
|
output(migration, !!options.json, migration.migrated
|
|
592
|
-
? `MIGRATE: PASS (${
|
|
593
|
-
: `MIGRATE: FAIL (${
|
|
721
|
+
? `MIGRATE: PASS (${target}) ${migration.fromFingerprint} -> ${migration.toFingerprint}`
|
|
722
|
+
: `MIGRATE: FAIL (${target}) ${migration.reason}`);
|
|
594
723
|
if (!migration.migrated)
|
|
595
724
|
process.exitCode = 1;
|
|
596
725
|
});
|
|
@@ -609,9 +738,26 @@ export function createProgram() {
|
|
|
609
738
|
if (!voidResult.voided)
|
|
610
739
|
process.exitCode = 1;
|
|
611
740
|
});
|
|
741
|
+
common(tdd.command('archive <test-id>'))
|
|
742
|
+
.requiredOption('--approver <name>', 'Human approver recording this archive')
|
|
743
|
+
.requiredOption('--reason <text>', 'Reason this TDD cycle is being archived')
|
|
744
|
+
.option('--confirm', 'Confirm the archive is reviewed and intended', false)
|
|
745
|
+
.action(async (testId, options) => {
|
|
746
|
+
if (!options.confirm)
|
|
747
|
+
throw new Error('Archiving a TDD cycle requires --confirm.');
|
|
748
|
+
const root = resolve(options.root);
|
|
749
|
+
const archiveResult = await archiveTddCycle(root, testId, options.approver, options.reason);
|
|
750
|
+
output(archiveResult, !!options.json, archiveResult.archived
|
|
751
|
+
? `ARCHIVE: PASS (${testId}) cycle=${archiveResult.cycleId}`
|
|
752
|
+
: `ARCHIVE: FAIL (${testId}) ${archiveResult.reason}`);
|
|
753
|
+
if (!archiveResult.archived)
|
|
754
|
+
process.exitCode = 1;
|
|
755
|
+
});
|
|
612
756
|
common(program.command('status').description('One-shot artifact and gate readiness summary'))
|
|
613
757
|
.action(async (options) => {
|
|
614
|
-
const
|
|
758
|
+
const root = resolve(options.root);
|
|
759
|
+
await assertCoordinatedEvidenceRead(root);
|
|
760
|
+
const status = await projectStatus(root);
|
|
615
761
|
const approvalSummary = status.approvals
|
|
616
762
|
? status.approvals.stages.map((stage) => `${stage.stage}=${stage.status}`).join(', ')
|
|
617
763
|
: 'unconfigured';
|
|
@@ -627,13 +773,40 @@ async function main() {
|
|
|
627
773
|
if (cause instanceof CommanderError && cause.exitCode === 0)
|
|
628
774
|
return;
|
|
629
775
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
630
|
-
if (process.argv.includes('--json'))
|
|
631
|
-
|
|
776
|
+
if (process.argv.includes('--json')) {
|
|
777
|
+
if (cause instanceof EvidenceWriterLockError) {
|
|
778
|
+
const underlying = cause.cause instanceof Error
|
|
779
|
+
? {
|
|
780
|
+
name: cause.cause.name,
|
|
781
|
+
message: cause.cause.message,
|
|
782
|
+
...('code' in cause.cause ? { code: String(cause.cause.code) } : {}),
|
|
783
|
+
}
|
|
784
|
+
: cause.cause === undefined ? undefined : { message: String(cause.cause) };
|
|
785
|
+
console.log(JSON.stringify({
|
|
786
|
+
error: {
|
|
787
|
+
code: cause.code,
|
|
788
|
+
message,
|
|
789
|
+
lockPath: cause.lockPath,
|
|
790
|
+
...(cause.owner === undefined ? {} : { owner: cause.owner }),
|
|
791
|
+
...(underlying === undefined ? {} : { cause: underlying }),
|
|
792
|
+
...(cause.guidance === undefined ? {} : { guidance: cause.guidance }),
|
|
793
|
+
},
|
|
794
|
+
}));
|
|
795
|
+
}
|
|
796
|
+
else if (cause instanceof EvidenceProtectedOutputError) {
|
|
797
|
+
console.log(JSON.stringify({ error: { code: cause.code, message, path: cause.path } }));
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
console.log(JSON.stringify({ error: { code: 'CLI_ERROR', message } }));
|
|
801
|
+
}
|
|
802
|
+
}
|
|
632
803
|
else
|
|
633
804
|
console.error(`musubix3: ${message}`);
|
|
634
805
|
process.exitCode = 2;
|
|
635
806
|
}
|
|
636
807
|
}
|
|
637
|
-
|
|
808
|
+
if (process.argv[1] !== undefined && pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url) {
|
|
809
|
+
await main();
|
|
810
|
+
}
|
|
638
811
|
import { readFile, stat } from 'node:fs/promises';
|
|
639
812
|
//# sourceMappingURL=main.js.map
|