@ionivetech/mugiwara 0.8.1 → 0.8.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.
package/src/integrity.ts CHANGED
@@ -162,23 +162,54 @@ export function checkTrail(missionDir: string, projectRoot: string): IntegrityIs
162
162
 
163
163
  // 3: evidence entries recorded as repo paths must exist
164
164
  const evidencePaths: string[] = [];
165
- const evidenceFile = join(missionDir, 'state.json');
166
- if (existsSync(evidenceFile)) {
165
+ // Solo layout writes state.json; team layout writes <member>.json per member.
166
+ // Reading only state.json left the evidence gate dead on the team path (B2).
167
+ const stateFiles = existsSync(missionDir)
168
+ ? readdirSync(missionDir)
169
+ .filter((n) => n.endsWith('.json') && n !== 'continue.json' && !n.startsWith('continue-'))
170
+ .sort()
171
+ : [];
172
+ for (const name of stateFiles) {
173
+ const evidenceFile = join(missionDir, name);
167
174
  try {
168
175
  const s = JSON.parse(readFileSync(evidenceFile, 'utf8')) as { evidence?: unknown };
169
- if (Array.isArray(s.evidence)) {
170
- for (const e of s.evidence) {
171
- if (typeof e !== 'string' || !e.trim()) continue;
172
- evidencePaths.push(e);
173
- const cand = join(projectRoot, e);
174
- if (!isAbsolute(e) && !existsSync(cand) && !existsSync(join(missionDir, e))) {
175
- issues.push({ kind: 'evidence', detail: `state.json evidence "${e}" does not exist` });
176
- }
176
+ if (!Array.isArray(s.evidence)) continue;
177
+ for (const e of s.evidence) {
178
+ if (typeof e !== 'string' || !e.trim()) continue;
179
+ evidencePaths.push(e);
180
+ const cand = join(projectRoot, e);
181
+ if (!isAbsolute(e) && !existsSync(cand) && !existsSync(join(missionDir, e))) {
182
+ issues.push({ kind: 'evidence', detail: `${name} evidence "${e}" does not exist` });
177
183
  }
178
184
  }
179
185
  } catch { /* corrupt state — the state reader owns that error */ }
180
186
  }
181
187
 
188
+ // Iron Law: no evidence = not complete. Absent evidence is a different failure
189
+ // from thin evidence, and previously went unreported entirely. (B7)
190
+ if (evidencePaths.length === 0) {
191
+ let severity: 'warn' | 'block' = 'warn';
192
+ try {
193
+ const policy = loadPolicy(projectRoot);
194
+ const lanes = (policy as unknown as { evidence?: { require_nonempty_for_lanes?: string[] } })?.evidence?.require_nonempty_for_lanes;
195
+ if (Array.isArray(lanes) && lanes.length) {
196
+ const stateLanes = new Set<string>();
197
+ for (const name of stateFiles) {
198
+ try {
199
+ const s = JSON.parse(readFileSync(join(missionDir, name), 'utf8')) as { lane?: unknown };
200
+ if (typeof s.lane === 'string') stateLanes.add(s.lane);
201
+ } catch { /* ignore */ }
202
+ }
203
+ if ([...stateLanes].some((l) => lanes.includes(l))) severity = 'block';
204
+ }
205
+ } catch { /* policy read failure -> keep warn */ }
206
+ issues.push({
207
+ kind: 'evidence',
208
+ severity,
209
+ detail: 'mission declares no evidence — closing with zero recorded checks',
210
+ });
211
+ }
212
+
182
213
  // 4: evidence-content spot check (T7): a PASS verdict that cites an evidence
183
214
  // path must point at a file that exists AND contains command-output shape
184
215
  // (backticked command or exit-status token). Fake-but-consistent trails
package/src/policy.ts CHANGED
@@ -19,7 +19,11 @@ export type MugiwaraPolicy = {
19
19
  coverage?: { new?: number; modified?: number };
20
20
  require_human_approval?: string[];
21
21
  };
22
- evidence?: { required?: string[] };
22
+ evidence?: {
23
+ required?: string[];
24
+ /** Lanes where an empty evidence set blocks archive instead of warning. (B7) */
25
+ require_nonempty_for_lanes?: string[];
26
+ };
23
27
  integrity?: { extra_secret_patterns?: Array<{ pattern: string; label: string; severity?: 'block' | 'warn' }> };
24
28
  attestation?: {
25
29
  required?: boolean;
@@ -361,7 +365,18 @@ function normalize(raw: Record<string, unknown>): MugiwaraPolicy {
361
365
  out.gates.require_human_approval = strings(gates.require_human_approval);
362
366
  }
363
367
  const evidence = raw.evidence as Record<string, unknown> | undefined;
364
- if (evidence && Array.isArray(evidence.required)) out.evidence = { required: strings(evidence.required) };
368
+ if (evidence) {
369
+ const ev: NonNullable<MugiwaraPolicy['evidence']> = {};
370
+ if (Array.isArray(evidence.required)) ev.required = strings(evidence.required);
371
+ else if (typeof evidence.required === 'string' && (evidence.required as string).trim().startsWith('[')) {
372
+ try { const p = JSON.parse(evidence.required as string); if (Array.isArray(p)) ev.required = strings(p); } catch { /* ignore */ }
373
+ }
374
+ if (Array.isArray(evidence.require_nonempty_for_lanes)) ev.require_nonempty_for_lanes = strings(evidence.require_nonempty_for_lanes);
375
+ else if (typeof evidence.require_nonempty_for_lanes === 'string' && (evidence.require_nonempty_for_lanes as string).trim().startsWith('[')) {
376
+ try { const p = JSON.parse(evidence.require_nonempty_for_lanes as string); if (Array.isArray(p)) ev.require_nonempty_for_lanes = strings(p); } catch { /* ignore */ }
377
+ }
378
+ if (ev.required || ev.require_nonempty_for_lanes) out.evidence = ev;
379
+ }
365
380
  const integrity = raw.integrity as Record<string, unknown> | undefined;
366
381
  if (integrity && Array.isArray(integrity.extra_secret_patterns)) {
367
382
  const arr = integrity.extra_secret_patterns as unknown[];