musubix3 0.1.1 → 0.1.2

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.
@@ -3,13 +3,13 @@
3
3
  "owner": { "name": "nahisaho" },
4
4
  "metadata": {
5
5
  "description": "GitHub Copilot CLI specification-driven development skills",
6
- "version": "0.1.1"
6
+ "version": "0.1.2"
7
7
  },
8
8
  "plugins": [
9
9
  {
10
10
  "name": "musubix3",
11
11
  "source": ".",
12
- "version": "0.1.1",
12
+ "version": "0.1.2",
13
13
  "description": "Evidence-driven SDD without duplicating native Copilot capabilities."
14
14
  }
15
15
  ]
@@ -46,11 +46,15 @@ After the work, run `npx musubix3 workflow-record sdd-implementation complete
46
46
  Put annotations in the authoritative source and test files, including
47
47
  supported non-JS/TS files; never add a proxy file just to increase coverage.
48
48
  An annotation establishes a link, not proof that code or tests are correct.
49
- 5. Run the project's actual focused tests/typecheck/build; fix failures.
49
+ 5. Before recording Red, run every configured `tdd.redPreflightCommands`
50
+ formatter/check and let musubix3 enforce that preflight. Then run the
51
+ project's actual focused tests/typecheck/build; fix failures.
50
52
  When mutation evidence is required, use the project's existing lightweight
51
53
  mutation mechanism to emit schema-v1 deterministic identities, requirement/
52
54
  test linkage, source/test SHA-256, operator/location, and killed status.
53
- Never fabricate results or add a large mutation dependency.
55
+ Never fabricate results or add a large mutation dependency. Use
56
+ `npx musubix3 mutation doctor --json` to inspect locally available engines
57
+ and configuration recommendations.
54
58
  6. Regenerate `npx musubix3 trace build`, check `trace check --strict`, then
55
59
  `npx musubix3 gate --changed`. Configure real command/argument arrays first.
56
60
  7. Use Copilot's native review and security-review capabilities when appropriate.
@@ -8,7 +8,6 @@ Follow the user's input language (日本語 / English). Use native Copilot revie
8
8
  security review for their specialist reasoning; this skill does not replace them.
9
9
  After the work, run `npx musubix3 workflow-record sdd-quality complete --status
10
10
  completed` exactly once.
11
-
12
11
  1. Inspect `.musubix/config.json` and `.musubix/policy-baseline.json` before
13
12
  running commands. Only execute trusted project configuration. Commands are
14
13
  executable/argument arrays, not shell text. Never edit the baseline without
@@ -18,17 +17,19 @@ completed` exactly once.
18
17
  custom report or a built-in Vitest/Jest, pytest, Go test, Cargo or JUnit
19
18
  adapter. Require executable native adapter contracts in CI; JUnit targets use
20
19
  an exact `@Tag("TEST-*")`, and pytest requires `pytest-json-report`. Review
21
- `requiredChecks`, coverage thresholds and architecture rules. Use strict
20
+ `requiredChecks`, `qualityProfile`, coverage thresholds and architecture rules.
21
+ Use `recommended` or `release` only when all profile requirements are
22
+ intentionally configured; profile validation never invents missing evidence. Use strict
22
23
  `codeGraph.mode` when unresolved computed module loading must block release,
23
24
  and ensure the trusted baseline prevents downgrading it. Configure a
24
25
  fresh structured `testReport` for `test-identities`. Do not weaken policy.
25
26
  3. Run `npx musubix3 gate --json` or `npx musubix3 gate --changed --json`.
26
- `npx musubix3 evidence refresh --json` is the explicit evidence-regeneration
27
- form of the same fail-closed gate pipeline. If `input-stability` fails,
27
+ `evidence refresh --json` runs the same fail-closed pipeline. If `input-stability` fails,
28
28
  inspect its per-path added/modified/deleted diagnostics and stop generators
29
29
  or formatters before rerunning. Standard dependency/build directories,
30
- including Cargo and Maven `target/`, are excluded; source-like generated
31
- inputs are not silently ignored.
30
+ including manifest-scoped Cargo and Maven `target/` and conventional `.venv`
31
+ or `venv` roots with a regular `pyvenv.cfg`, are excluded; arbitrary source
32
+ directories and source-like generated inputs are not silently ignored.
32
33
  Changed mode reports Git changes and dependent files but conservatively runs
33
34
  all checks, including commands. It never treats unrun checks as successful.
34
35
  When `tdd` is required, confirm every command has test-scoped `tddArgs`, every
@@ -40,8 +41,9 @@ completed` exactly once.
40
41
  Treat workflow records as declarations until `workflow-verify` binds each
41
42
  completed declaration to one distinct completed Copilot Skill tool call.
42
43
  In strict mode require one final successful result, matching session UUID,
43
- causal timestamps, bounded freshness, complete tool lifecycles and canonical
44
- transcript hash. Ensure the baseline protects strict/session/freshness policy.
44
+ causal transcript order, policy-bounded concurrent-event clock skew, bounded
45
+ freshness, complete tool lifecycles and canonical transcript hash. Ensure the
46
+ baseline protects strict/session/freshness/event-skew policy.
45
47
  When `change-history` is required,
46
48
  confirm ordered checkpoints, chained TDD records, requirement-scoped Code
47
49
  Graph changes, exact CHANGE requirement enumeration, measurable Acceptance,
@@ -59,7 +61,8 @@ completed` exactly once.
59
61
  identities bound to must functional requirements, current source/test
60
62
  fingerprints, operator/location, authoritative tests, and killed results.
61
63
  Reject survived, skipped, duplicate, conflicting, stale, or unlinked mutants.
62
- musubix3 validates evidence; it does not bundle a mutation engine.
64
+ Run `mutation doctor` for language-aware local probes and remediation;
65
+ musubix3 validates evidence and does not bundle a mutation engine.
63
66
  Missing required tools, commands, artifacts or evidence block readiness.
64
67
  5. Use native review/security-review as needed, recording their findings
65
68
  separately from machine evidence. Never fabricate review or test results.
@@ -72,4 +75,4 @@ completed` exactly once.
72
75
  Ensure the baseline protects CI-required mode, strict OIDC and key binding.
73
76
  Never store a private key or overstate OIDC as proof of arbitrary runner work.
74
77
  7. Run `npx musubix3 status --json`; stale evidence must be refreshed. Report
75
- limitations and residual risks before handoff. No resident watcher or REPL.
78
+ limitations and residual risks before handoff; no resident watcher or REPL.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.2 - 2026-09-07
6
+
7
+ - Exclude conventional `.venv` and `venv` Python environments identified by a
8
+ regular `pyvenv.cfg` file from project snapshots and Code Graph indexing,
9
+ without allowing the marker to hide arbitrary source directories.
10
+ - Add configurable `tdd.redPreflightCommands` so formatters and other plain
11
+ preflight commands complete before a Red test fingerprint is captured.
12
+ - Add explicit `custom`, `minimal`, `recommended`, and `release` quality
13
+ profiles with fail-closed requirements for stronger profiles.
14
+ - Accept bounded timestamp skew for causally ordered concurrent Copilot tool
15
+ events in strict workflow verification, with a policy-protected
16
+ `workflow.maxEventSkewMs` limit.
17
+ - Add `mutation doctor` with language-aware local engine probes, attempted
18
+ commands, and actionable configuration recommendations.
19
+
5
20
  ## 0.1.1 - 2026-09-07
6
21
 
7
22
  - Preserve fail-closed input-stability checks while reporting every added,
package/README-ja.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # musubix3
2
2
 
3
- **v0.1.1 · GitHub Copilot CLI 専用 · Node.js ≥20 · TypeScript · MIT**
3
+ **最新公開版 v0.1.2 · GitHub Copilot CLI 専用 · Node.js ≥20 · TypeScript · MIT**
4
4
 
5
5
  [English](README.md)
6
6
 
@@ -19,16 +19,16 @@ ID の接続や SAT 判定だけで、実装の正しさを保証するもので
19
19
  対象プロジェクトで、公開済みパッケージを実行します。
20
20
 
21
21
  ```sh
22
- npx musubix3@0.1.1 --version
23
- npx musubix3@0.1.1 init --dry-run
24
- npx musubix3@0.1.1 init
22
+ npx musubix3@0.1.2 --version
23
+ npx musubix3@0.1.2 init --dry-run
24
+ npx musubix3@0.1.2 init
25
25
  copilot
26
26
  ```
27
27
 
28
28
  バージョンを固定してプロジェクトへインストールする場合は、次を実行します。
29
29
 
30
30
  ```sh
31
- npm install --save-dev --save-exact musubix3@0.1.1
31
+ npm install --save-dev --save-exact musubix3@0.1.2
32
32
  npx --no-install musubix3 --version
33
33
  npx --no-install musubix3 init --dry-run
34
34
  npx --no-install musubix3 init
@@ -176,6 +176,7 @@ npx musubix3 status --json
176
176
  | `knowledge query <text> [--limit 10]` | TF-IDF/cosine のランキングと陳腐化情報 |
177
177
  | `formal generate <file> [--format both\|smt2\|lean]` | SHA-256付きの再現可能なsolver入力生成 |
178
178
  | `formal doctor` | Z3、Lean、`lake env lean` の存在とバージョン確認 |
179
+ | `mutation doctor` | 言語別のローカルmutation engine確認と導入推奨 |
179
180
  | `formal check <file> [--solver auto\|none\|z3\|lean]` | 明示的なBoolean・条件・数値・時間・状態遷移モデルを検査 |
180
181
  | `model-correspondence validate` | Formal JSON→生成trace→正本passing testの証拠を再検証 |
181
182
  | `evidence refresh [--changed]` | 同じfail-closed gate pipelineで派生証拠を再生成 |
@@ -327,6 +328,7 @@ version: 1.0.0
327
328
  {
328
329
  "schemaVersion": 1,
329
330
  "language": "auto",
331
+ "qualityProfile": "custom",
330
332
  "commands": [
331
333
  { "name": "typecheck", "command": "npm", "args": ["run", "typecheck"], "required": true, "timeoutMs": 120000 },
332
334
  {
@@ -342,10 +344,12 @@ version: 1.0.0
342
344
  "thresholds": { "design": 1, "implementation": 1, "tests": 1 },
343
345
  "formal": { "solver": "none", "minModeledFraction": 0, "timeoutMs": 12000 },
344
346
  "mutation": { "mode": "compatible" },
347
+ "tdd": { "redPreflightCommands": [] },
345
348
  "workflow": {
346
349
  "mode": "compatible",
347
350
  "maxAgeSeconds": 3600,
348
- "maxFutureSkewSeconds": 60
351
+ "maxFutureSkewSeconds": 60,
352
+ "maxEventSkewMs": 1000
349
353
  },
350
354
  "attestation": {
351
355
  "mode": "local",
@@ -366,6 +370,15 @@ version: 1.0.0
366
370
 
367
371
  未知の設定キー、不正な閾値、重複コマンドはエラーです。
368
372
  glob は `*` / `**` / `?` に対応し、外部依存は `npm:` 接頭辞で表現します。
373
+ `qualityProfile`の既定値は`custom`です。`minimal`はSDDの基本gate、
374
+ `recommended`はstrict Code Graph、TDD、構造化test identityも要求し、
375
+ `release`はformal、mutation、workflow、変更、performance、CI attestationを
376
+ 含む完全なrelease checkを要求します。不足設定や弱い設定を証拠で補ったことにはしません。
377
+ `tdd.redPreflightCommands`にはformatter等のplain command名を指定でき、
378
+ Redのtest fingerprintを取得する前に成功が必須です。
379
+ 通常ファイルの`pyvenv.cfg`を含む`.venv`と`venv`はsnapshotと
380
+ Code Graphから除外されますが、任意のsource directoryにmarkerを置いても
381
+ 除外されません。
369
382
  `codeGraph.mode` の既定値は `compatible` で、未解決の計算された
370
383
  `import()` / `require()` は警告です。`strict` にするとグラフゲートを阻止する
371
384
  エラーになります。信頼済みbaselineが `strict` の場合、`compatible` への
@@ -386,7 +399,9 @@ command reportでpassしたことを要求します。欠落・改変・stale・
386
399
 
387
400
  `.musubix/policy-baseline.json` は必須チェック、閾値、アーキテクチャ、
388
401
  Formalポリシー、mutation mode、workflow strict/session/freshness、CI必須attestation、
389
- strict OIDC identity/key binding、必須コマンド名の最低条件です。弱体化は拒否され、
402
+ strict OIDC identity/key binding、必須コマンド名の最低条件です。Red preflightを
403
+ 要求するbaselineは正規化済み`commands`定義も保持し、同名formatterへの
404
+ すり替えを拒否します。弱体化は拒否され、
390
405
  変更ゲート中のbaseline変更には独立承認が必要です。CODEOWNERS等で保護してください。
391
406
 
392
407
  **信頼した設定だけを実行してください。** ゲートは環境変数を継承し、シェルを
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # musubix3
2
2
 
3
- **v0.1.1 · GitHub Copilot CLI only · Node.js ≥20 · TypeScript · MIT**
3
+ **Latest published v0.1.2 · GitHub Copilot CLI only · Node.js ≥20 · TypeScript · MIT**
4
4
 
5
5
  [日本語](README-ja.md)
6
6
 
@@ -21,16 +21,16 @@ or requirements are satisfiable.
21
21
  Run the published package from the target project:
22
22
 
23
23
  ```sh
24
- npx musubix3@0.1.1 --version
25
- npx musubix3@0.1.1 init --dry-run
26
- npx musubix3@0.1.1 init
24
+ npx musubix3@0.1.2 --version
25
+ npx musubix3@0.1.2 init --dry-run
26
+ npx musubix3@0.1.2 init
27
27
  copilot
28
28
  ```
29
29
 
30
30
  For a reproducible project-local installation:
31
31
 
32
32
  ```sh
33
- npm install --save-dev --save-exact musubix3@0.1.1
33
+ npm install --save-dev --save-exact musubix3@0.1.2
34
34
  npx --no-install musubix3 --version
35
35
  npx --no-install musubix3 init --dry-run
36
36
  npx --no-install musubix3 init
@@ -184,6 +184,7 @@ validation/gate or requested solver failure, **2** usage, I/O or malformed confi
184
184
  | `knowledge query <text> [--limit 10]` | Deterministic TF-IDF/cosine results and staleness flag |
185
185
  | `formal generate <file> [--format both\|smt2\|lean]` | Reproducible solver inputs with SHA-256 evidence |
186
186
  | `formal doctor` | Probe Z3, Lean, and `lake env lean` availability and versions |
187
+ | `mutation doctor` | Probe language-aware local mutation engines and show setup recommendations |
187
188
  | `formal check <file> [--solver auto\|none\|z3\|lean]` | Check the explicit Boolean/conditional/numeric/temporal/transition model |
188
189
  | `model-correspondence validate` | Revalidate Formal JSON → generated trace → authoritative passing test evidence |
189
190
  | `evidence refresh [--changed]` | Regenerate derived evidence through the same fail-closed gate pipeline |
@@ -328,6 +329,7 @@ Example `.musubix/config.json` (adapt command arguments to your own project):
328
329
  {
329
330
  "schemaVersion": 1,
330
331
  "language": "auto",
332
+ "qualityProfile": "custom",
331
333
  "commands": [
332
334
  { "name": "typecheck", "command": "npm", "args": ["run", "typecheck"], "required": true, "timeoutMs": 120000 },
333
335
  {
@@ -343,10 +345,12 @@ Example `.musubix/config.json` (adapt command arguments to your own project):
343
345
  "thresholds": { "design": 1, "implementation": 1, "tests": 1 },
344
346
  "formal": { "solver": "none", "minModeledFraction": 0, "timeoutMs": 12000 },
345
347
  "mutation": { "mode": "compatible" },
348
+ "tdd": { "redPreflightCommands": [] },
346
349
  "workflow": {
347
350
  "mode": "compatible",
348
351
  "maxAgeSeconds": 3600,
349
- "maxFutureSkewSeconds": 60
352
+ "maxFutureSkewSeconds": 60,
353
+ "maxEventSkewMs": 1000
350
354
  },
351
355
  "attestation": {
352
356
  "mode": "local",
@@ -367,6 +371,16 @@ Example `.musubix/config.json` (adapt command arguments to your own project):
367
371
 
368
372
  Config is validated strictly; misspelled keys, invalid bounds and duplicate
369
373
  commands fail closed. Globs support `*`, `**`, `?`; external imports use `npm:`.
374
+ `qualityProfile` is `custom` by default. `minimal` preserves the core SDD gate,
375
+ `recommended` also requires strict Code Graph, TDD and structured test
376
+ identities, and `release` requires the complete formal, mutation, workflow,
377
+ change, performance and CI-attestation checks. Stronger profiles reject missing
378
+ or weakened settings rather than silently filling in evidence.
379
+ Use `tdd.redPreflightCommands` to reference plain configured formatter commands;
380
+ they must pass before Red captures the authoritative test fingerprint.
381
+ Conventional `.venv` and `venv` Python environments containing a regular
382
+ `pyvenv.cfg` are excluded from source snapshots and Code Graph indexing.
383
+ Arbitrary source directories containing that marker remain tracked.
370
384
  `codeGraph.mode` defaults to `compatible`, where unresolved computed
371
385
  `import()`/`require()` calls remain warnings. Set it to `strict` to make those
372
386
  diagnostics gate-blocking errors. A trusted strict policy baseline prevents
@@ -391,7 +405,9 @@ diagnostic codes are stable English; human status labels include Japanese.
391
405
  `.musubix/policy-baseline.json` records minimum required checks, coverage,
392
406
  architecture, formal policy, mutation mode, workflow strict/session/freshness settings,
393
407
  CI-required attestation and strict OIDC identity/key binding, and required
394
- command names. Weakening is rejected; changing the baseline in `gate --changed`
408
+ command names. A baseline that requires TDD Red preflights must also include
409
+ their normalized `commands` definitions, which prevents replacing a trusted
410
+ formatter while retaining its name. Weakening is rejected; changing the baseline in `gate --changed`
395
411
  requires independent approval. Protect the baseline with review/CODEOWNERS.
396
412
 
397
413
  **Only run trusted configuration**: gates execute its commands with inherited
@@ -55,6 +55,7 @@ export interface WorkflowConfig {
55
55
  expectedSessionId?: string;
56
56
  maxAgeSeconds?: number;
57
57
  maxFutureSkewSeconds?: number;
58
+ maxEventSkewMs?: number;
58
59
  }
59
60
  export interface CodeGraphConfig {
60
61
  mode: 'compatible' | 'strict';
@@ -62,9 +63,14 @@ export interface CodeGraphConfig {
62
63
  export interface MutationConfig {
63
64
  mode: 'compatible' | 'strict';
64
65
  }
66
+ export interface TddConfig {
67
+ redPreflightCommands: string[];
68
+ }
69
+ export type QualityProfile = 'custom' | 'minimal' | 'recommended' | 'release';
65
70
  export interface Config {
66
71
  schemaVersion: 1;
67
72
  language: 'auto' | 'en' | 'ja';
73
+ qualityProfile: QualityProfile;
68
74
  commands: CommandConfig[];
69
75
  requiredChecks: string[];
70
76
  thresholds: {
@@ -79,17 +85,21 @@ export interface Config {
79
85
  codeGraph: CodeGraphConfig;
80
86
  formal: FormalConfig;
81
87
  mutation: MutationConfig;
88
+ tdd: TddConfig;
82
89
  workflow: WorkflowConfig;
83
90
  attestation: AttestationConfig;
84
91
  }
85
92
  export interface PolicyBaseline {
86
93
  schemaVersion: 1;
94
+ qualityProfile: QualityProfile;
95
+ commands: CommandConfig[];
87
96
  requiredChecks: string[];
88
97
  thresholds: Config['thresholds'];
89
98
  architecture: Config['architecture'];
90
99
  codeGraph: Config['codeGraph'];
91
100
  formal: Pick<FormalConfig, 'solver' | 'minModeledFraction'>;
92
101
  mutation: MutationConfig;
102
+ tdd: TddConfig;
93
103
  workflow: WorkflowConfig;
94
104
  attestation: AttestationConfig;
95
105
  requiredCommands: string[];
@@ -4,6 +4,7 @@ export const checkNames = ['requirements', 'design', 'constitution', 'trace', 'g
4
4
  export const defaultConfig = {
5
5
  schemaVersion: 1,
6
6
  language: 'auto',
7
+ qualityProfile: 'custom',
7
8
  commands: [],
8
9
  requiredChecks: ['requirements', 'design', 'constitution', 'trace', 'graph', 'commands'],
9
10
  thresholds: { design: 1, implementation: 1, tests: 1 },
@@ -11,7 +12,8 @@ export const defaultConfig = {
11
12
  codeGraph: { mode: 'compatible' },
12
13
  formal: { solver: 'none', minModeledFraction: 0, timeoutMs: 12_000 },
13
14
  mutation: { mode: 'compatible' },
14
- workflow: { mode: 'compatible', maxAgeSeconds: 3600, maxFutureSkewSeconds: 60 },
15
+ tdd: { redPreflightCommands: [] },
16
+ workflow: { mode: 'compatible', maxAgeSeconds: 3600, maxFutureSkewSeconds: 60, maxEventSkewMs: 1000 },
15
17
  attestation: {
16
18
  mode: 'local',
17
19
  maxAgeSeconds: 3600,
@@ -22,12 +24,15 @@ export const defaultConfig = {
22
24
  };
23
25
  export const defaultPolicyBaseline = {
24
26
  schemaVersion: 1,
27
+ qualityProfile: defaultConfig.qualityProfile,
28
+ commands: [],
25
29
  requiredChecks: [...defaultConfig.requiredChecks],
26
30
  thresholds: { ...defaultConfig.thresholds },
27
31
  architecture: { forbidCycles: defaultConfig.architecture.forbidCycles, rules: [...defaultConfig.architecture.rules] },
28
32
  codeGraph: { ...defaultConfig.codeGraph },
29
33
  formal: { solver: defaultConfig.formal.solver, minModeledFraction: defaultConfig.formal.minModeledFraction },
30
34
  mutation: { ...defaultConfig.mutation },
35
+ tdd: { redPreflightCommands: [...defaultConfig.tdd.redPreflightCommands] },
31
36
  workflow: { ...defaultConfig.workflow },
32
37
  attestation: {
33
38
  ...defaultConfig.attestation,
@@ -71,14 +76,51 @@ function validateAdapterArgs(adapter, args, name) {
71
76
  throw new Error(`command ${name} args contain adapter-owned argument ${conflict}; remove it because the ${adapter} adapter adds targeted test/report arguments.`);
72
77
  }
73
78
  }
79
+ const profileRequiredChecks = {
80
+ minimal: ['requirements', 'design', 'constitution', 'trace', 'graph', 'commands'],
81
+ recommended: ['requirements', 'design', 'constitution', 'trace', 'graph', 'commands', 'test-identities', 'tdd'],
82
+ release: [
83
+ 'requirements', 'design', 'constitution', 'trace', 'graph', 'formal', 'model-correspondence',
84
+ 'mutation', 'workflow', 'tdd', 'change-history', 'change-completeness', 'performance',
85
+ 'attestation', 'test-identities', 'commands',
86
+ ],
87
+ };
88
+ function validateQualityProfile(config) {
89
+ if (config.qualityProfile === 'custom')
90
+ return;
91
+ const missing = profileRequiredChecks[config.qualityProfile]
92
+ .filter((check) => !config.requiredChecks.includes(check));
93
+ if (missing.length) {
94
+ throw new Error(`qualityProfile ${config.qualityProfile} requires checks: ${missing.join(', ')}.`);
95
+ }
96
+ if (config.qualityProfile !== 'minimal' && config.codeGraph.mode !== 'strict') {
97
+ throw new Error(`qualityProfile ${config.qualityProfile} requires codeGraph.mode strict.`);
98
+ }
99
+ if (config.qualityProfile === 'release') {
100
+ if (config.formal.solver === 'none' || config.formal.minModeledFraction <= 0) {
101
+ throw new Error('qualityProfile release requires a formal solver and positive modeled coverage.');
102
+ }
103
+ if (config.mutation.mode !== 'strict')
104
+ throw new Error('qualityProfile release requires mutation.mode strict.');
105
+ if (config.workflow.mode !== 'strict')
106
+ throw new Error('qualityProfile release requires workflow.mode strict.');
107
+ if (config.attestation.mode !== 'ci-required') {
108
+ throw new Error('qualityProfile release requires attestation.mode ci-required.');
109
+ }
110
+ }
111
+ }
74
112
  export function parseConfig(input) {
75
113
  const value = object(input, 'config');
76
- keys(value, ['schemaVersion', 'language', 'commands', 'requiredChecks', 'thresholds', 'architecture', 'codeGraph', 'formal', 'mutation', 'workflow', 'attestation'], 'config');
114
+ keys(value, ['schemaVersion', 'language', 'qualityProfile', 'commands', 'requiredChecks', 'thresholds', 'architecture', 'codeGraph', 'formal', 'mutation', 'tdd', 'workflow', 'attestation'], 'config');
77
115
  if (value.schemaVersion !== 1)
78
116
  throw new Error('Unsupported config schemaVersion; expected 1.');
79
117
  const language = optional(value.language, 'auto');
80
118
  if (!['auto', 'en', 'ja'].includes(String(language)))
81
119
  throw new Error('language must be auto, en, or ja.');
120
+ const qualityProfile = optional(value.qualityProfile, defaultConfig.qualityProfile);
121
+ if (!['custom', 'minimal', 'recommended', 'release'].includes(String(qualityProfile))) {
122
+ throw new Error('qualityProfile must be custom, minimal, recommended, or release.');
123
+ }
82
124
  const requiredChecks = strings(optional(value.requiredChecks, defaultConfig.requiredChecks), 'requiredChecks');
83
125
  if (requiredChecks.some((s) => !checkNames.includes(s)))
84
126
  throw new Error('Unknown required check.');
@@ -178,6 +220,20 @@ export function parseConfig(input) {
178
220
  });
179
221
  if (new Set(commands.map((c) => c.name)).size !== commands.length)
180
222
  throw new Error('Command names must be unique.');
223
+ const rawTdd = object(optional(value.tdd, {}), 'tdd');
224
+ keys(rawTdd, ['redPreflightCommands'], 'tdd');
225
+ const redPreflightCommands = strings(optional(rawTdd.redPreflightCommands, defaultConfig.tdd.redPreflightCommands), 'tdd.redPreflightCommands');
226
+ if (new Set(redPreflightCommands).size !== redPreflightCommands.length) {
227
+ throw new Error('tdd.redPreflightCommands must not contain duplicates.');
228
+ }
229
+ for (const name of redPreflightCommands) {
230
+ const command = commands.find((candidate) => candidate.name === name);
231
+ if (!command)
232
+ throw new Error(`tdd.redPreflightCommands references unknown command ${name}.`);
233
+ if (command.adapter || command.testReport || command.tddReport || command.mutationReport) {
234
+ throw new Error(`TDD Red preflight command ${name} must be a plain command without test or mutation reports.`);
235
+ }
236
+ }
181
237
  const architecture = object(optional(value.architecture, {}), 'architecture');
182
238
  keys(architecture, ['forbidCycles', 'rules'], 'architecture');
183
239
  if (architecture.forbidCycles !== undefined && typeof architecture.forbidCycles !== 'boolean')
@@ -218,7 +274,7 @@ export function parseConfig(input) {
218
274
  throw new Error('mutation.mode must be compatible or strict.');
219
275
  }
220
276
  const rawWorkflow = object(optional(value.workflow, {}), 'workflow');
221
- keys(rawWorkflow, ['mode', 'expectedSessionId', 'maxAgeSeconds', 'maxFutureSkewSeconds'], 'workflow');
277
+ keys(rawWorkflow, ['mode', 'expectedSessionId', 'maxAgeSeconds', 'maxFutureSkewSeconds', 'maxEventSkewMs'], 'workflow');
222
278
  const workflowMode = optional(rawWorkflow.mode, defaultConfig.workflow.mode);
223
279
  if (!['compatible', 'strict'].includes(String(workflowMode))) {
224
280
  throw new Error('workflow.mode must be compatible or strict.');
@@ -241,6 +297,11 @@ export function parseConfig(input) {
241
297
  || workflowMaxFutureSkewSeconds < 0 || workflowMaxFutureSkewSeconds > 600) {
242
298
  throw new Error('workflow.maxFutureSkewSeconds must be 0..600.');
243
299
  }
300
+ const workflowMaxEventSkewMs = optional(rawWorkflow.maxEventSkewMs, defaultConfig.workflow.maxEventSkewMs);
301
+ if (typeof workflowMaxEventSkewMs !== 'number' || !Number.isInteger(workflowMaxEventSkewMs)
302
+ || workflowMaxEventSkewMs < 0 || workflowMaxEventSkewMs > 60_000) {
303
+ throw new Error('workflow.maxEventSkewMs must be 0..60000.');
304
+ }
244
305
  const rawAttestation = object(optional(value.attestation, {}), 'attestation');
245
306
  keys(rawAttestation, [
246
307
  'mode', 'repository', 'maxAgeSeconds', 'maxFutureSkewSeconds', 'trustedPublicKeys', 'githubOidc',
@@ -307,17 +368,24 @@ export function parseConfig(input) {
307
368
  && rawAttestation.repository !== rawGithubOidc.repository) {
308
369
  throw new Error('attestation.githubOidc.repository must match attestation.repository.');
309
370
  }
310
- return {
311
- schemaVersion: 1, language: language, commands, requiredChecks, thresholds,
371
+ const config = {
372
+ schemaVersion: 1,
373
+ language: language,
374
+ qualityProfile: qualityProfile,
375
+ commands,
376
+ requiredChecks,
377
+ thresholds,
312
378
  architecture: { forbidCycles: architecture.forbidCycles !== false, rules },
313
379
  codeGraph: { mode: codeGraphMode },
314
380
  formal: { solver: solver, minModeledFraction, timeoutMs: formalTimeoutMs },
315
381
  mutation: { mode: mutationMode },
382
+ tdd: { redPreflightCommands },
316
383
  workflow: {
317
384
  mode: workflowMode,
318
385
  ...(typeof rawWorkflow.expectedSessionId === 'string' ? { expectedSessionId: rawWorkflow.expectedSessionId } : {}),
319
386
  maxAgeSeconds: workflowMaxAgeSeconds,
320
387
  maxFutureSkewSeconds: workflowMaxFutureSkewSeconds,
388
+ maxEventSkewMs: workflowMaxEventSkewMs,
321
389
  },
322
390
  attestation: {
323
391
  mode: mode,
@@ -338,6 +406,8 @@ export function parseConfig(input) {
338
406
  : { mode: 'off' },
339
407
  },
340
408
  };
409
+ validateQualityProfile(config);
410
+ return config;
341
411
  }
342
412
  export async function loadConfig(root) {
343
413
  const path = '.musubix/config.json';
@@ -348,31 +418,36 @@ export async function loadConfig(root) {
348
418
  export function parsePolicyBaseline(input) {
349
419
  const value = object(input, 'policy baseline');
350
420
  keys(value, [
351
- 'schemaVersion', 'requiredChecks', 'thresholds', 'architecture', 'codeGraph', 'formal',
352
- 'mutation', 'workflow', 'attestation', 'requiredCommands',
421
+ 'schemaVersion', 'qualityProfile', 'commands', 'requiredChecks', 'thresholds', 'architecture', 'codeGraph', 'formal',
422
+ 'mutation', 'tdd', 'workflow', 'attestation', 'requiredCommands',
353
423
  ], 'policy baseline');
354
424
  if (value.schemaVersion !== 1)
355
425
  throw new Error('Unsupported policy baseline schemaVersion; expected 1.');
356
426
  const parsed = parseConfig({
357
427
  schemaVersion: 1,
428
+ qualityProfile: value.qualityProfile,
358
429
  requiredChecks: value.requiredChecks,
359
430
  thresholds: value.thresholds,
360
431
  architecture: value.architecture,
361
432
  codeGraph: value.codeGraph,
362
433
  formal: value.formal,
363
434
  mutation: value.mutation,
435
+ tdd: value.tdd,
364
436
  workflow: value.workflow,
365
437
  attestation: value.attestation,
366
- commands: [],
438
+ commands: value.commands,
367
439
  });
368
440
  return {
369
441
  schemaVersion: 1,
442
+ qualityProfile: parsed.qualityProfile,
443
+ commands: parsed.commands,
370
444
  requiredChecks: parsed.requiredChecks,
371
445
  thresholds: parsed.thresholds,
372
446
  architecture: parsed.architecture,
373
447
  codeGraph: parsed.codeGraph,
374
448
  formal: { solver: parsed.formal.solver, minModeledFraction: parsed.formal.minModeledFraction },
375
449
  mutation: parsed.mutation,
450
+ tdd: parsed.tdd,
376
451
  workflow: parsed.workflow,
377
452
  attestation: parsed.attestation,
378
453
  requiredCommands: strings(optional(value.requiredCommands, []), 'requiredCommands'),
@@ -386,6 +461,10 @@ export async function loadPolicyBaseline(root) {
386
461
  }
387
462
  export function policyDiagnostics(config, baseline, changed = null) {
388
463
  const diagnostics = [];
464
+ const profileRank = { custom: 0, minimal: 1, recommended: 2, release: 3 };
465
+ if (profileRank[config.qualityProfile] < profileRank[baseline.qualityProfile]) {
466
+ diagnostics.push(error('POLICY_QUALITY_PROFILE', `qualityProfile cannot be weakened below ${baseline.qualityProfile}.`));
467
+ }
389
468
  for (const check of baseline.requiredChecks) {
390
469
  if (!config.requiredChecks.includes(check))
391
470
  diagnostics.push(error('POLICY_REQUIRED_CHECK', `Required check ${check} was removed from the trusted baseline.`));
@@ -413,6 +492,16 @@ export function policyDiagnostics(config, baseline, changed = null) {
413
492
  if (baseline.mutation.mode === 'strict' && config.mutation.mode !== 'strict') {
414
493
  diagnostics.push(error('POLICY_MUTATION_MODE', 'mutation.mode cannot be weakened below strict as required by the trusted baseline.'));
415
494
  }
495
+ for (const command of baseline.tdd.redPreflightCommands) {
496
+ const baselineCommand = baseline.commands.find((candidate) => candidate.name === command);
497
+ const configuredCommand = config.commands.find((candidate) => candidate.name === command);
498
+ if (!config.tdd.redPreflightCommands.includes(command) || !configuredCommand) {
499
+ diagnostics.push(error('POLICY_TDD_PREFLIGHT', `TDD Red preflight command ${command} was removed from the trusted baseline.`));
500
+ }
501
+ else if (baselineCommand && JSON.stringify(configuredCommand) !== JSON.stringify(baselineCommand)) {
502
+ diagnostics.push(error('POLICY_TDD_PREFLIGHT', `TDD Red preflight command ${command} differs from the trusted baseline definition.`));
503
+ }
504
+ }
416
505
  if (baseline.workflow.mode === 'strict') {
417
506
  if (config.workflow.mode !== 'strict') {
418
507
  diagnostics.push(error('POLICY_WORKFLOW_MODE', 'workflow.mode cannot be weakened below strict as required by the trusted baseline.'));
@@ -427,6 +516,9 @@ export function policyDiagnostics(config, baseline, changed = null) {
427
516
  if ((config.workflow.maxFutureSkewSeconds ?? 60) > (baseline.workflow.maxFutureSkewSeconds ?? 60)) {
428
517
  diagnostics.push(error('POLICY_WORKFLOW_FUTURE_SKEW', 'workflow.maxFutureSkewSeconds is weaker than the trusted baseline.'));
429
518
  }
519
+ if ((config.workflow.maxEventSkewMs ?? 1000) > (baseline.workflow.maxEventSkewMs ?? 1000)) {
520
+ diagnostics.push(error('POLICY_WORKFLOW_EVENT_SKEW', 'workflow.maxEventSkewMs is weaker than the trusted baseline.'));
521
+ }
430
522
  }
431
523
  if (baseline.attestation.mode === 'ci-required') {
432
524
  if (config.attestation.mode !== 'ci-required') {