@mmnto/cli 1.91.0 → 1.93.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/dist/commands/doctor-parity.d.ts +105 -1
- package/dist/commands/doctor-parity.d.ts.map +1 -1
- package/dist/commands/doctor-parity.js +406 -34
- package/dist/commands/doctor-parity.js.map +1 -1
- package/dist/commands/doctor-parity.test.js +423 -2
- package/dist/commands/doctor-parity.test.js.map +1 -1
- package/dist/commands/ecl-gc.test.js +20 -0
- package/dist/commands/ecl-gc.test.js.map +1 -1
- package/dist/commands/init-templates.d.ts +6 -2
- package/dist/commands/init-templates.d.ts.map +1 -1
- package/dist/commands/init-templates.js +63 -2
- package/dist/commands/init-templates.js.map +1 -1
- package/dist/commands/init.test.js +78 -3
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/mail.d.ts.map +1 -1
- package/dist/commands/mail.js +36 -0
- package/dist/commands/mail.js.map +1 -1
- package/dist/commands/mail.test.js +77 -0
- package/dist/commands/mail.test.js.map +1 -1
- package/dist/commands/review-fan.d.ts +336 -0
- package/dist/commands/review-fan.d.ts.map +1 -0
- package/dist/commands/review-fan.js +1076 -0
- package/dist/commands/review-fan.js.map +1 -0
- package/dist/commands/review-fan.test.d.ts +2 -0
- package/dist/commands/review-fan.test.d.ts.map +1 -0
- package/dist/commands/review-fan.test.js +1184 -0
- package/dist/commands/review-fan.test.js.map +1 -0
- package/dist/commands/shield-covariate.test.d.ts +14 -0
- package/dist/commands/shield-covariate.test.d.ts.map +1 -0
- package/dist/commands/shield-covariate.test.js +84 -0
- package/dist/commands/shield-covariate.test.js.map +1 -0
- package/dist/commands/shield-eval.integration.test.js +57 -13
- package/dist/commands/shield-eval.integration.test.js.map +1 -1
- package/dist/commands/shield.d.ts +162 -3
- package/dist/commands/shield.d.ts.map +1 -1
- package/dist/commands/shield.js +342 -74
- package/dist/commands/shield.js.map +1 -1
- package/dist/commands/shield.test.js +169 -3
- package/dist/commands/shield.test.js.map +1 -1
- package/dist/git.d.ts +25 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +50 -6
- package/dist/git.js.map +1 -1
- package/dist/git.test.js +119 -14
- package/dist/git.test.js.map +1 -1
- package/dist/index.js +24 -2
- package/dist/index.js.map +1 -1
- package/dist/orchestrators/orchestrator.d.ts +1 -0
- package/dist/orchestrators/orchestrator.d.ts.map +1 -1
- package/dist/orchestrators/orchestrator.js +1 -1
- package/dist/orchestrators/orchestrator.js.map +1 -1
- package/package.json +2 -2
|
@@ -36,6 +36,15 @@
|
|
|
36
36
|
* unconfigured → exactly one `skip` line; configured-but-missing / unparseable /
|
|
37
37
|
* unsupported-schema → `warn`, never a crash. Dynamic-import `@mmnto/totem` to
|
|
38
38
|
* keep core off the CLI cold-start graph, matching the other doctor checks.
|
|
39
|
+
*
|
|
40
|
+
* The **trust-readout** (mmnto-ai/totem#2327, Prop 303 §5(a)) post-processes
|
|
41
|
+
* the flat per-line dump into the doctor's aggregate output contract: verdict
|
|
42
|
+
* rollup (per-seat + global, R1), the run-time coverage denominator (R2),
|
|
43
|
+
* why-not per non-pass row at the level probed (R3), the `--json` verdict
|
|
44
|
+
* artifact (R4), and the `--strict` declaredly-toothless honesty line (R5).
|
|
45
|
+
* Spec: mmnto-ai/totem-strategy:doctrine/parity-manifest.md § "The
|
|
46
|
+
* trust-readout — the doctor's output contract"; deltas raise there, never
|
|
47
|
+
* silently diverge. Pure post-processing — zero probes added.
|
|
39
48
|
*/
|
|
40
49
|
import * as path from 'node:path';
|
|
41
50
|
// init-templates (large canonical strings) + the node:url / node:module builtins
|
|
@@ -46,9 +55,10 @@ const CHECK_NAME = 'Parity';
|
|
|
46
55
|
* Resolve the on-disk artifact(s) a mechanical contract checks, or `undefined`
|
|
47
56
|
* when this slice doesn't handle the contract (hooks / presence / value-equality
|
|
48
57
|
* → kept as `skip` stubs). `claude-skills` yields one artifact PER distributed
|
|
49
|
-
* skill; `review-reply-skill-content`
|
|
50
|
-
* overlaps `claude-skills` by design — both are
|
|
51
|
-
* the same file; flagged to strategy as a manifest
|
|
58
|
+
* skill; `review-reply-skill-content` and `review-loop-skill-content` are the
|
|
59
|
+
* single per-skill contracts (each overlaps `claude-skills` by design — both are
|
|
60
|
+
* distinct manifest contracts on the same file; flagged to strategy as a manifest
|
|
61
|
+
* observation).
|
|
52
62
|
*
|
|
53
63
|
* The canonical block is extracted from the running CLI's OWN `init-templates`
|
|
54
64
|
* export (passed-in `extract` is the core `extractManagedBlock`, dynamic-imported
|
|
@@ -73,6 +83,15 @@ function mechanicalArtifactsFor(contractId, gitRoot, extract, templates) {
|
|
|
73
83
|
lineName: 'Parity: review-reply-skill-content',
|
|
74
84
|
},
|
|
75
85
|
];
|
|
86
|
+
case 'review-loop-skill-content':
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
consumerPath: path.join(gitRoot, '.claude', 'skills', 'review-loop', 'SKILL.md'),
|
|
90
|
+
markers,
|
|
91
|
+
canonicalBlock: extract(templates.reviewLoopContent, markers),
|
|
92
|
+
lineName: 'Parity: review-loop-skill-content',
|
|
93
|
+
},
|
|
94
|
+
];
|
|
76
95
|
default:
|
|
77
96
|
return undefined;
|
|
78
97
|
}
|
|
@@ -347,7 +366,7 @@ export async function checkParity(cwd) {
|
|
|
347
366
|
name: CHECK_NAME,
|
|
348
367
|
status: 'skip',
|
|
349
368
|
message: 'no parity manifest configured',
|
|
350
|
-
}, configured);
|
|
369
|
+
}, configured, 'not-configured');
|
|
351
370
|
case 'not-found': {
|
|
352
371
|
// mmnto-ai/totem#2094 — when the configured manifest path lives under
|
|
353
372
|
// node_modules/ (the strategy-doctrine optional-pin shape), the normal
|
|
@@ -368,7 +387,7 @@ export async function checkParity(cwd) {
|
|
|
368
387
|
status: 'warn',
|
|
369
388
|
message: `parity manifest not found at ${rel(cwd, result.path)}`,
|
|
370
389
|
remediation,
|
|
371
|
-
}, configured);
|
|
390
|
+
}, configured, 'not-found');
|
|
372
391
|
}
|
|
373
392
|
case 'unparseable':
|
|
374
393
|
return single({
|
|
@@ -376,14 +395,14 @@ export async function checkParity(cwd) {
|
|
|
376
395
|
status: 'warn',
|
|
377
396
|
message: `parity manifest unreadable at ${rel(cwd, result.path)}: ${result.reason}`,
|
|
378
397
|
remediation: 'Fix the manifest YAML / schema, then re-run totem doctor --parity.',
|
|
379
|
-
}, configured);
|
|
398
|
+
}, configured, 'unparseable');
|
|
380
399
|
case 'unsupported-schema':
|
|
381
400
|
return single({
|
|
382
401
|
name: CHECK_NAME,
|
|
383
402
|
status: 'warn',
|
|
384
403
|
message: `parity manifest schema v${result.schemaVersion} unsupported (this doctor supports v${SUPPORTED_PARITY_SCHEMA_VERSION})`,
|
|
385
404
|
remediation: 'Upgrade @mmnto/cli or align the manifest schema-version.',
|
|
386
|
-
}, configured);
|
|
405
|
+
}, configured, 'unsupported-schema');
|
|
387
406
|
case 'ok': {
|
|
388
407
|
const { contracts } = result.manifest;
|
|
389
408
|
const summary = {
|
|
@@ -396,10 +415,11 @@ export async function checkParity(cwd) {
|
|
|
396
415
|
// (packages/cli lazy-load guideline).
|
|
397
416
|
const { createRequire } = await import('node:module');
|
|
398
417
|
const url = await import('node:url');
|
|
399
|
-
const { DISTRIBUTED_CLAUDE_SKILLS, REVIEW_REPLY_SKILL_CONTENT, SKILL_MARKER_START, SKILL_MARKER_END, CLAUDE_SESSION_START, GEMINI_SESSION_START, SESSION_START_MARKER, } = await import('./init-templates.js');
|
|
418
|
+
const { DISTRIBUTED_CLAUDE_SKILLS, REVIEW_REPLY_SKILL_CONTENT, REVIEW_LOOP_SKILL_CONTENT, SKILL_MARKER_START, SKILL_MARKER_END, CLAUDE_SESSION_START, GEMINI_SESSION_START, SESSION_START_MARKER, } = await import('./init-templates.js');
|
|
400
419
|
const skillTemplates = {
|
|
401
420
|
distributedSkills: DISTRIBUTED_CLAUDE_SKILLS,
|
|
402
421
|
reviewReplyContent: REVIEW_REPLY_SKILL_CONTENT,
|
|
422
|
+
reviewLoopContent: REVIEW_LOOP_SKILL_CONTENT,
|
|
403
423
|
markers: { start: SKILL_MARKER_START, end: SKILL_MARKER_END },
|
|
404
424
|
};
|
|
405
425
|
// The running CLI's own whole-file SessionStart hook templates (static — no
|
|
@@ -436,6 +456,10 @@ export async function checkParity(cwd) {
|
|
|
436
456
|
// regenerated git-hook canonical matches THIS repo's package manager — no false drift.
|
|
437
457
|
const fallbackCmd = getFallbackCommand(gitRoot);
|
|
438
458
|
const blockingDriftIds = [];
|
|
459
|
+
// Trust-readout per-contract metadata (mmnto-ai/totem#2327), tagged at the
|
|
460
|
+
// routing branch that owns each contract so the R2 coverage split derives
|
|
461
|
+
// from the run itself — never from a mirrored registry (Tenet 20).
|
|
462
|
+
const readoutMeta = {};
|
|
439
463
|
// The consumers-scope guard shared by the routing branches that don't
|
|
440
464
|
// self-guard in core (mechanical + capability-probe): a scoped contract
|
|
441
465
|
// must not emit drift in a repo that is not an intended consumer.
|
|
@@ -471,10 +495,20 @@ export async function checkParity(cwd) {
|
|
|
471
495
|
// NON-probe rungs (managed-block, version-pin, …) are informational —
|
|
472
496
|
// they fall through to the existing tractability routing unchanged.
|
|
473
497
|
if (c.manifestation === 'capability-probe') {
|
|
498
|
+
// Probe resolution BEFORE the scope guard is meta-only (pure switch, no
|
|
499
|
+
// probe executes): a scoped-out row still classifies by whether the
|
|
500
|
+
// registry implements it (#2327 R2).
|
|
501
|
+
const probes = capabilityProbesFor(c.id, gitRoot);
|
|
502
|
+
readoutMeta[c.id] =
|
|
503
|
+
probes === undefined
|
|
504
|
+
? { coverage: 'honest-absent' }
|
|
505
|
+
: {
|
|
506
|
+
coverage: 'mechanical',
|
|
507
|
+
...(probes[0] !== undefined ? { sensesProbed: probes[0].probedLevel } : {}),
|
|
508
|
+
};
|
|
474
509
|
const scopeSkip = consumersSkip(c);
|
|
475
510
|
if (scopeSkip !== undefined)
|
|
476
511
|
return scopeSkip;
|
|
477
|
-
const probes = capabilityProbesFor(c.id, gitRoot);
|
|
478
512
|
if (probes === undefined) {
|
|
479
513
|
return [
|
|
480
514
|
stub(c, `${c.dimension} (capability-probe) — probe not yet implemented for this row`),
|
|
@@ -510,6 +544,11 @@ export async function checkParity(cwd) {
|
|
|
510
544
|
// documented to avoid.
|
|
511
545
|
if (c.manifestation === 'value-equality') {
|
|
512
546
|
const fields = valueEqualityFieldsFor(c.id, gitRoot);
|
|
547
|
+
// The scalar read is a config-declaration probe (#2327 R3): 'declared'.
|
|
548
|
+
readoutMeta[c.id] =
|
|
549
|
+
fields === undefined
|
|
550
|
+
? { coverage: 'honest-absent' }
|
|
551
|
+
: { coverage: 'mechanical', sensesProbed: 'declared' };
|
|
513
552
|
if (fields === undefined) {
|
|
514
553
|
return [
|
|
515
554
|
stub(c, `${c.dimension} (value-equality) — drift detection not yet implemented for this row`),
|
|
@@ -538,6 +577,11 @@ export async function checkParity(cwd) {
|
|
|
538
577
|
// contract id at most ONCE for --strict so the count reflects contracts.
|
|
539
578
|
if (c.manifestation === 'content-hash') {
|
|
540
579
|
const packageDir = lockContentPackageDirFor(c.id, gitRoot);
|
|
580
|
+
// Installed-artifact hash equality probes on-disk content (#2327 R3): 'present'.
|
|
581
|
+
readoutMeta[c.id] =
|
|
582
|
+
packageDir === undefined
|
|
583
|
+
? { coverage: 'honest-absent' }
|
|
584
|
+
: { coverage: 'mechanical', sensesProbed: 'present' };
|
|
541
585
|
if (packageDir === undefined) {
|
|
542
586
|
return [
|
|
543
587
|
stub(c, `${c.dimension} (content-hash) — drift detection not yet implemented for this row`),
|
|
@@ -558,6 +602,7 @@ export async function checkParity(cwd) {
|
|
|
558
602
|
// Fail-loud PER ROW, never per manifest (the total-outage guard): an
|
|
559
603
|
// unrecognized rung value surfaces verbatim instead of darking the
|
|
560
604
|
// sensor or silently mis-routing.
|
|
605
|
+
readoutMeta[c.id] = { coverage: 'honest-absent' };
|
|
561
606
|
return [
|
|
562
607
|
stub(c, `manifestation '${c.manifestation}' unrecognized by this doctor — drift detection not yet implemented`),
|
|
563
608
|
];
|
|
@@ -570,10 +615,13 @@ export async function checkParity(cwd) {
|
|
|
570
615
|
// self-routes those to the toolchain reader (mmnto-ai/totem#2115). Only
|
|
571
616
|
// stub a version-pinned row that's neither a deps package nor a toolchain.
|
|
572
617
|
if (packageName === undefined && c.dimension !== TOOLCHAIN_DIMENSION) {
|
|
618
|
+
readoutMeta[c.id] = { coverage: 'honest-absent' };
|
|
573
619
|
return [
|
|
574
620
|
stub(c, `${c.dimension} (version-pinned) — drift detection not yet implemented`),
|
|
575
621
|
];
|
|
576
622
|
}
|
|
623
|
+
// Pin-currency reads the consumer's dependency declaration (#2327 R3): 'declared'.
|
|
624
|
+
readoutMeta[c.id] = { coverage: 'mechanical', sensesProbed: 'declared' };
|
|
577
625
|
const verdict = detectVersionPinnedContract(c, { cwd, gitRoot, repoId, packageName });
|
|
578
626
|
if (verdict.status === 'warn' && c.blocking === true)
|
|
579
627
|
blockingDriftIds.push(c.id);
|
|
@@ -581,6 +629,17 @@ export async function checkParity(cwd) {
|
|
|
581
629
|
}
|
|
582
630
|
// ── mechanical content-equality (mmnto-ai/totem#2073) ──
|
|
583
631
|
if (c.tractability === 'mechanical') {
|
|
632
|
+
// Skills resolution is pure (no probe executes) — resolved BEFORE the
|
|
633
|
+
// scope guard so a scoped-out row still classifies by whether the
|
|
634
|
+
// registry implements it (#2327 R2); reused as `artifacts` below.
|
|
635
|
+
const skillArtifacts = mechanicalArtifactsFor(c.id, gitRoot, extractManagedBlock, skillTemplates);
|
|
636
|
+
const mechanicalImplemented = c.id === 'git-hooks' ||
|
|
637
|
+
c.id === 'session-start-orientation' ||
|
|
638
|
+
skillArtifacts !== undefined;
|
|
639
|
+
// Content-equality probes on-disk artifacts (#2327 R3): 'present'.
|
|
640
|
+
readoutMeta[c.id] = mechanicalImplemented
|
|
641
|
+
? { coverage: 'mechanical', sensesProbed: 'present' }
|
|
642
|
+
: { coverage: 'honest-absent' };
|
|
584
643
|
// Honor the contract's `consumers` scope before sensing drift (mirrors
|
|
585
644
|
// detectVersionPinnedContract, which self-guards). A scoped mechanical
|
|
586
645
|
// contract must not emit drift — or, under --strict, fail — in a repo that
|
|
@@ -632,8 +691,9 @@ export async function checkParity(cwd) {
|
|
|
632
691
|
blockingDriftIds.push(c.id);
|
|
633
692
|
return lines;
|
|
634
693
|
}
|
|
635
|
-
// skills: managed-block content equality against the in-process template
|
|
636
|
-
|
|
694
|
+
// skills: managed-block content equality against the in-process template
|
|
695
|
+
// (resolved above, pre-scope-guard, for the readout classification).
|
|
696
|
+
const artifacts = skillArtifacts;
|
|
637
697
|
if (artifacts === undefined) {
|
|
638
698
|
return [
|
|
639
699
|
stub(c, `${c.dimension} (mechanical) — drift detection not yet implemented for this sub-class`),
|
|
@@ -665,6 +725,9 @@ export async function checkParity(cwd) {
|
|
|
665
725
|
// `info` can't enter blockingDriftIds, so these contracts cannot fail even
|
|
666
726
|
// under --strict (the manifest's "never fails" contract).
|
|
667
727
|
if (c.tractability === 'manual-attestation') {
|
|
728
|
+
// Declared-covered but mechanically uncovered (#2327 R2 — human-asserted
|
|
729
|
+
// judgment is not code-verified truth); nothing is probed.
|
|
730
|
+
readoutMeta[c.id] = { coverage: 'attestation-only' };
|
|
668
731
|
// The detector reads the sub-class discriminant (`c.package`) + the
|
|
669
732
|
// canonical source directly off the contract. `package:` set ⇒ vendor-SDK
|
|
670
733
|
// pin read; unset ⇒ doctrine-row pure-info surface. `attested` carries the
|
|
@@ -678,17 +741,31 @@ export async function checkParity(cwd) {
|
|
|
678
741
|
return [verdictToLine(c, verdict)];
|
|
679
742
|
}
|
|
680
743
|
// ── anything else (a future tractability) → skip stub; the slice boundary stays observable ──
|
|
744
|
+
readoutMeta[c.id] = { coverage: 'honest-absent' };
|
|
681
745
|
return [
|
|
682
746
|
stub(c, `${c.dimension} (${c.tractability}) — drift detection not yet implemented`),
|
|
683
747
|
];
|
|
684
748
|
});
|
|
685
|
-
return {
|
|
749
|
+
return {
|
|
750
|
+
results: [summary, ...perContract],
|
|
751
|
+
blockingDriftIds,
|
|
752
|
+
configured,
|
|
753
|
+
loadStatus: 'ok',
|
|
754
|
+
readout: {
|
|
755
|
+
manifest: {
|
|
756
|
+
schemaVersion: result.manifest.schemaVersion,
|
|
757
|
+
status: result.manifest.status,
|
|
758
|
+
},
|
|
759
|
+
contracts,
|
|
760
|
+
meta: readoutMeta,
|
|
761
|
+
},
|
|
762
|
+
};
|
|
686
763
|
}
|
|
687
764
|
}
|
|
688
765
|
}
|
|
689
|
-
/** Wrap a single summary line in the `ParityCheckResult` shape (no blocking ids). */
|
|
690
|
-
function single(result, configured) {
|
|
691
|
-
return { results: [result], blockingDriftIds: [], configured };
|
|
766
|
+
/** Wrap a single summary line in the `ParityCheckResult` shape (no blocking ids, no readout). */
|
|
767
|
+
function single(result, configured, loadStatus) {
|
|
768
|
+
return { results: [result], blockingDriftIds: [], configured, loadStatus };
|
|
692
769
|
}
|
|
693
770
|
/** Map a core `ParityContractVerdict` to a `ParityLine` keyed by the contract id. */
|
|
694
771
|
function verdictToLine(contract, verdict) {
|
|
@@ -726,6 +803,231 @@ function rel(cwd, target) {
|
|
|
726
803
|
const r = path.relative(cwd, target);
|
|
727
804
|
return r.length > 0 ? r : target;
|
|
728
805
|
}
|
|
806
|
+
// ─── Trust-readout assembly (mmnto-ai/totem#2327) ────────
|
|
807
|
+
/** The `--json` artifact's own version stamp (`readout-schema-version`). */
|
|
808
|
+
const READOUT_SCHEMA_VERSION = 1;
|
|
809
|
+
/**
|
|
810
|
+
* R2 claim-boundary sentence — the readout states its own scope in its own
|
|
811
|
+
* output: an active-but-unmanifested surface (an undeclared MCP server, a
|
|
812
|
+
* user-global plugin) is outside the claim, stated not implied.
|
|
813
|
+
*/
|
|
814
|
+
const CLAIM_BOUNDARY = 'covers the manifest-declared contract set only; active-but-unmanifested surfaces are outside this claim';
|
|
815
|
+
/**
|
|
816
|
+
* Whether a verdict line belongs to a `--strict`-promotable contract. A
|
|
817
|
+
* contract's drift can render across MULTIPLE artifact lines (e.g.
|
|
818
|
+
* `Parity: claude-skills (signoff)`), so a line is promotable when its name is
|
|
819
|
+
* the `Parity: <id>` summary OR a `Parity: <id> (…)` artifact line — an
|
|
820
|
+
* exact-name Set would leave the artifact lines rendered as WARN while the
|
|
821
|
+
* command still exits non-zero (GCA review). The trailing space guards against
|
|
822
|
+
* a contract id that is a prefix of another. Shared by the CLI render and the
|
|
823
|
+
* readout builder so the two can't disagree on what promoted.
|
|
824
|
+
*/
|
|
825
|
+
function isPromotableLineName(name, blockingDriftIds) {
|
|
826
|
+
return blockingDriftIds.some((id) => name === `Parity: ${id}` || name.startsWith(`Parity: ${id} `));
|
|
827
|
+
}
|
|
828
|
+
/** Resolve the contract a verdict line belongs to (same prefix rule as promotion). */
|
|
829
|
+
function contractForLineName(name, contracts) {
|
|
830
|
+
return contracts.find((c) => name === `Parity: ${c.id}` || name.startsWith(`Parity: ${c.id} `));
|
|
831
|
+
}
|
|
832
|
+
// The CLI-side stubs' "not yet implemented" message shape — the one code-owned
|
|
833
|
+
// string the classifier keys on. A future optional reasonClass on the core
|
|
834
|
+
// verdict type retires this coupling (flagged to strategy in the
|
|
835
|
+
// 2026-07-09T2256Z deltas dispatch as an observation).
|
|
836
|
+
const HONEST_ABSENT_RE = /not yet implemented/;
|
|
837
|
+
/**
|
|
838
|
+
* R3 reason class for one verdict line. `pass` omits its class. A `skip` is
|
|
839
|
+
* `honest-absent` when the stub message or the contract's coverage class says
|
|
840
|
+
* the detector is unbuilt; every other skip — scope-guard skips and
|
|
841
|
+
* scope-indeterminate skips (e.g. applicable-but-missing scaffolds) alike —
|
|
842
|
+
* classifies `scoping-skip`, never silently `honest-absent`, which would
|
|
843
|
+
* understate what the registry implements.
|
|
844
|
+
*/
|
|
845
|
+
function reasonClassFor(verdict, message, coverage) {
|
|
846
|
+
switch (verdict) {
|
|
847
|
+
case 'pass':
|
|
848
|
+
return undefined;
|
|
849
|
+
case 'warn':
|
|
850
|
+
case 'fail':
|
|
851
|
+
return 'drift';
|
|
852
|
+
case 'info':
|
|
853
|
+
return 'attestation';
|
|
854
|
+
case 'unknown':
|
|
855
|
+
return 'detector-error';
|
|
856
|
+
case 'skip':
|
|
857
|
+
if (HONEST_ABSENT_RE.test(message) || coverage === 'honest-absent')
|
|
858
|
+
return 'honest-absent';
|
|
859
|
+
return 'scoping-skip';
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Assemble the trust-readout from `checkParity`'s raw materials — pure
|
|
864
|
+
* post-processing over the existing detector output (Prop 303 non-goal 2:
|
|
865
|
+
* zero probes added here).
|
|
866
|
+
*/
|
|
867
|
+
export function buildParityReadout(inputs, results, blockingDriftIds, strict) {
|
|
868
|
+
const { contracts, meta } = inputs;
|
|
869
|
+
const rows = [];
|
|
870
|
+
for (const line of results) {
|
|
871
|
+
const contract = contractForLineName(line.name, contracts);
|
|
872
|
+
// The section summary line (`Parity`) is not a contract row.
|
|
873
|
+
if (contract === undefined)
|
|
874
|
+
continue;
|
|
875
|
+
const promoted = strict && line.status === 'warn' && isPromotableLineName(line.name, blockingDriftIds);
|
|
876
|
+
const verdict = promoted ? 'fail' : line.status;
|
|
877
|
+
const m = meta[contract.id];
|
|
878
|
+
const reasonClass = reasonClassFor(verdict, line.message, m?.coverage);
|
|
879
|
+
rows.push({
|
|
880
|
+
id: contract.id,
|
|
881
|
+
verdict,
|
|
882
|
+
...(m?.sensesProbed !== undefined ? { sensesProbed: m.sensesProbed } : {}),
|
|
883
|
+
...(reasonClass !== undefined ? { reasonClass } : {}),
|
|
884
|
+
message: line.message,
|
|
885
|
+
...(contract.lastAttested !== undefined ? { lastAttested: contract.lastAttested } : {}),
|
|
886
|
+
lineName: line.name,
|
|
887
|
+
skippedNotGated: verdict === 'skip' && contract.blocking === true,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
const zero = () => ({ pass: 0, warn: 0, info: 0, unknown: 0, skip: 0, fail: 0 });
|
|
891
|
+
const global = zero();
|
|
892
|
+
for (const r of rows)
|
|
893
|
+
global[r.verdict] += 1;
|
|
894
|
+
// Seats = the sorted union of declared vendor-adapter values. A line counts
|
|
895
|
+
// toward seat S when its contract is vendor-neutral (no vendor-adapter — it
|
|
896
|
+
// manifests on every seat) or declares S. An EMPTY vendor-adapter list is
|
|
897
|
+
// treated as vendor-neutral too: the schema admits `[]`, and excluding such
|
|
898
|
+
// a row from every seat while counting it globally would reopen the exact
|
|
899
|
+
// per-seat honesty hole R1 exists to close (spec-owner review on #2328).
|
|
900
|
+
// Per-seat exists precisely so a seat-scoped skip cannot hide inside the
|
|
901
|
+
// global number (R1).
|
|
902
|
+
const seats = [...new Set(contracts.flatMap((c) => c.vendorAdapter ?? []))].sort();
|
|
903
|
+
const perSeat = {};
|
|
904
|
+
for (const seat of seats)
|
|
905
|
+
perSeat[seat] = zero();
|
|
906
|
+
const adapterById = new Map(contracts.map((c) => [c.id, c.vendorAdapter]));
|
|
907
|
+
for (const r of rows) {
|
|
908
|
+
const adapter = adapterById.get(r.id);
|
|
909
|
+
for (const seat of seats) {
|
|
910
|
+
if (adapter === undefined || adapter.length === 0 || adapter.includes(seat)) {
|
|
911
|
+
perSeat[seat][r.verdict] += 1;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
// R2: the denominator counts CONTRACTS (registry ∩ manifest is contract-
|
|
916
|
+
// granular), while the rollup above counts rendered verdict LINES — two
|
|
917
|
+
// populations, named apart in the render (raised Delta 2).
|
|
918
|
+
const denominator = { mechanical: 0, attestationOnly: 0, honestAbsent: 0 };
|
|
919
|
+
for (const c of contracts) {
|
|
920
|
+
const cls = meta[c.id]?.coverage ?? 'honest-absent';
|
|
921
|
+
if (cls === 'mechanical')
|
|
922
|
+
denominator.mechanical += 1;
|
|
923
|
+
else if (cls === 'attestation-only')
|
|
924
|
+
denominator.attestationOnly += 1;
|
|
925
|
+
else
|
|
926
|
+
denominator.honestAbsent += 1;
|
|
927
|
+
}
|
|
928
|
+
// R5: blocking-ids = the manifest's DECLARED `blocking: true` set, derived
|
|
929
|
+
// from the yaml at run time (never from prose — Tenet 20); gates-anything =
|
|
930
|
+
// that set is non-empty. Which blocking contracts actually drifted is
|
|
931
|
+
// visible as `fail` rows (strict) / promotable warns (default).
|
|
932
|
+
const blockingIds = contracts.filter((c) => c.blocking === true).map((c) => c.id);
|
|
933
|
+
return {
|
|
934
|
+
manifest: inputs.manifest,
|
|
935
|
+
rollup: { global, perSeat },
|
|
936
|
+
denominator,
|
|
937
|
+
strict: { armed: strict, blockingIds, gatesAnything: blockingIds.length > 0 },
|
|
938
|
+
rows,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
/** One-line count rendering shared by the global + per-seat rollup lines. */
|
|
942
|
+
function renderCounts(c) {
|
|
943
|
+
return `${c.pass} pass · ${c.warn} warn · ${c.info} info · ${c.unknown} unknown · ${c.skip} skip · ${c.fail} fail`;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* R3 attestation-age fragment: days since `last-attested:` ("not recorded"
|
|
947
|
+
* when absent). Staleness refines the message, never the status (the existing
|
|
948
|
+
* manifest constraint, unchanged).
|
|
949
|
+
*/
|
|
950
|
+
function attestationAge(lastAttested) {
|
|
951
|
+
if (lastAttested === undefined)
|
|
952
|
+
return 'last attested: not recorded';
|
|
953
|
+
const t = Date.parse(lastAttested);
|
|
954
|
+
if (Number.isNaN(t))
|
|
955
|
+
return `last attested: ${lastAttested} (unparseable date)`;
|
|
956
|
+
const days = Math.max(0, Math.floor((Date.now() - t) / 86_400_000));
|
|
957
|
+
return `last attested ${days} day(s) ago`;
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* The `--json` verdict artifact (R4) — field names fixed by the spec section
|
|
961
|
+
* (kebab-case, no local envelope: the spec's top-level shape overrides the
|
|
962
|
+
* `json-output.ts` success/error wrapper). Emitted bare on stdout so the
|
|
963
|
+
* artifact is diffable (Prop 302 verdict-artifact discipline). On degenerate
|
|
964
|
+
* manifest-load states the artifact carries the load status and empty
|
|
965
|
+
* rollup/rows — the honest "nothing to roll up" shape.
|
|
966
|
+
*/
|
|
967
|
+
function readoutJsonArtifact(readout, loadStatus, strict) {
|
|
968
|
+
const countsJson = (c) => ({
|
|
969
|
+
pass: c.pass,
|
|
970
|
+
warn: c.warn,
|
|
971
|
+
info: c.info,
|
|
972
|
+
unknown: c.unknown,
|
|
973
|
+
skip: c.skip,
|
|
974
|
+
fail: c.fail,
|
|
975
|
+
});
|
|
976
|
+
if (readout === undefined) {
|
|
977
|
+
const zero = { pass: 0, warn: 0, info: 0, unknown: 0, skip: 0, fail: 0 };
|
|
978
|
+
return {
|
|
979
|
+
'readout-schema-version': READOUT_SCHEMA_VERSION,
|
|
980
|
+
manifest: { status: loadStatus },
|
|
981
|
+
rollup: { global: countsJson(zero), 'per-seat': {} },
|
|
982
|
+
denominator: {
|
|
983
|
+
mechanical: 0,
|
|
984
|
+
'attestation-only': 0,
|
|
985
|
+
'honest-absent': 0,
|
|
986
|
+
'claim-boundary': CLAIM_BOUNDARY,
|
|
987
|
+
},
|
|
988
|
+
strict: { armed: strict, 'blocking-ids': [], 'gates-anything': false },
|
|
989
|
+
rows: [],
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
return {
|
|
993
|
+
'readout-schema-version': READOUT_SCHEMA_VERSION,
|
|
994
|
+
manifest: {
|
|
995
|
+
'schema-version': readout.manifest.schemaVersion,
|
|
996
|
+
status: readout.manifest.status,
|
|
997
|
+
},
|
|
998
|
+
rollup: {
|
|
999
|
+
global: countsJson(readout.rollup.global),
|
|
1000
|
+
'per-seat': Object.fromEntries(Object.entries(readout.rollup.perSeat).map(([seat, c]) => [seat, countsJson(c)])),
|
|
1001
|
+
},
|
|
1002
|
+
denominator: {
|
|
1003
|
+
mechanical: readout.denominator.mechanical,
|
|
1004
|
+
'attestation-only': readout.denominator.attestationOnly,
|
|
1005
|
+
'honest-absent': readout.denominator.honestAbsent,
|
|
1006
|
+
'claim-boundary': CLAIM_BOUNDARY,
|
|
1007
|
+
},
|
|
1008
|
+
strict: {
|
|
1009
|
+
armed: readout.strict.armed,
|
|
1010
|
+
'blocking-ids': readout.strict.blockingIds,
|
|
1011
|
+
'gates-anything': readout.strict.gatesAnything,
|
|
1012
|
+
},
|
|
1013
|
+
rows: readout.rows.map((r) => ({
|
|
1014
|
+
id: r.id,
|
|
1015
|
+
verdict: r.verdict,
|
|
1016
|
+
...(r.sensesProbed !== undefined ? { 'senses-probed': r.sensesProbed } : {}),
|
|
1017
|
+
...(r.reasonClass !== undefined ? { 'reason-class': r.reasonClass } : {}),
|
|
1018
|
+
message: r.message,
|
|
1019
|
+
...(r.lastAttested !== undefined ? { 'last-attested': r.lastAttested } : {}),
|
|
1020
|
+
// R5 (build delta 4, mmnto-ai/totem-strategy#851): a qualifying row —
|
|
1021
|
+
// `verdict: skip` on a `blocking: true` contract (id ∈ strict.blocking-ids)
|
|
1022
|
+
// — carries `skipped-not-gated: true`. Presence-only, mirroring the
|
|
1023
|
+
// `last-attested?` convention: emitted true or the key is ABSENT, never
|
|
1024
|
+
// `false`. The fact is derivable, but R5's never-a-silent-pass posture
|
|
1025
|
+
// states it outright in the spec-shaped artifact rather than making every
|
|
1026
|
+
// consumer join two sections. Additive — `readout-schema-version` stays 1.
|
|
1027
|
+
...(r.skippedNotGated ? { 'skipped-not-gated': true } : {}),
|
|
1028
|
+
})),
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
729
1031
|
// ─── CLI entry ──────────────────────────────────────────
|
|
730
1032
|
// Same value as CHECK_NAME — aliased (not re-literal'd) so the two can't drift.
|
|
731
1033
|
const TAG = CHECK_NAME;
|
|
@@ -747,23 +1049,50 @@ export async function doctorParityCliCommand(options = {}) {
|
|
|
747
1049
|
.replace(/ {2,}/g, ' ')
|
|
748
1050
|
.trim();
|
|
749
1051
|
const cwd = options.cwdForTest ?? process.cwd();
|
|
750
|
-
const { results, blockingDriftIds, configured } = await checkParity(cwd);
|
|
1052
|
+
const { results, blockingDriftIds, configured, loadStatus, readout: readoutInputs, } = await checkParity(cwd);
|
|
751
1053
|
// Folded-into-`--strict` no-op: when this run is the strict fold (not an explicit
|
|
752
1054
|
// `doctor --parity`) and no repo-local manifest is configured, render and gate
|
|
753
1055
|
// nothing — a non-adopter's `doctor --strict` stays byte-identical to before the
|
|
754
1056
|
// fold. Explicit `--parity` (onlyWhenConfigured omitted) still shows the SKIP.
|
|
755
1057
|
if (options.onlyWhenConfigured && !configured)
|
|
756
1058
|
return;
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
|
|
1059
|
+
const strict = options.strict === true;
|
|
1060
|
+
const readout = readoutInputs !== undefined
|
|
1061
|
+
? buildParityReadout(readoutInputs, results, blockingDriftIds, strict)
|
|
1062
|
+
: undefined;
|
|
1063
|
+
// `--json` replaces the human render wholesale: the artifact owns stdout so
|
|
1064
|
+
// it stays diffable (R4). The strict throw below still applies — artifact
|
|
1065
|
+
// AND exit code, matching R5's "exits non-zero iff ≥1 fail".
|
|
1066
|
+
if (options.json) {
|
|
1067
|
+
process.stdout.write(JSON.stringify(readoutJsonArtifact(readout, loadStatus, strict), null, 2) + '\n');
|
|
1068
|
+
}
|
|
1069
|
+
else {
|
|
1070
|
+
renderParityHuman(results, blockingDriftIds, strict, { log, bold, errorColor, successColor, warnColor }, render);
|
|
1071
|
+
if (readout !== undefined) {
|
|
1072
|
+
renderTrustReadout(readout, { log, bold }, render);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
// Sensor-not-gate: drift is report-only by default (exit 0). Only `--strict`
|
|
1076
|
+
// promotes a `blocking: true` contract's drift to a non-zero exit; non-blocking
|
|
1077
|
+
// drift never gates, and `info`/`unknown` are never promoted. The detectors
|
|
1078
|
+
// emit no raw `fail` status, so the gate is purely the strict+blocking
|
|
1079
|
+
// promotion — the gating model for a future slice that DOES emit a `fail` is
|
|
1080
|
+
// settled when that slice lands (CR review mmnto-ai/totem#2071: keep the gate from
|
|
1081
|
+
// suggesting a non-strict path that would break sensor-not-gate).
|
|
1082
|
+
if (options.strict && blockingDriftIds.length > 0) {
|
|
1083
|
+
throw new TotemError('PARITY_DRIFT_DETECTED', `${blockingDriftIds.length} parity contract(s) reported blocking drift under --strict.`, 'Reconcile each blocking contract against its canonical source, then re-run totem doctor --parity --strict.');
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
/** The pre-readout flat per-line render (unchanged output, extracted for the `--json` split). */
|
|
1087
|
+
function renderParityHuman(results, blockingDriftIds, strict, ui, render) {
|
|
1088
|
+
const { log, bold } = ui;
|
|
1089
|
+
const errorColor = ui.errorColor ?? ((s) => s);
|
|
1090
|
+
const successColor = ui.successColor ?? ((s) => s);
|
|
1091
|
+
const warnColor = ui.warnColor ?? ((s) => s);
|
|
765
1092
|
for (const r of results) {
|
|
766
|
-
const status =
|
|
1093
|
+
const status = strict && r.status === 'warn' && isPromotableLineName(r.name, blockingDriftIds)
|
|
1094
|
+
? 'fail'
|
|
1095
|
+
: r.status;
|
|
767
1096
|
switch (status) {
|
|
768
1097
|
case 'pass':
|
|
769
1098
|
log.success(TAG, `${successColor(bold('PASS'))} — ${render(r.message)}`);
|
|
@@ -799,15 +1128,58 @@ export async function doctorParityCliCommand(options = {}) {
|
|
|
799
1128
|
break;
|
|
800
1129
|
}
|
|
801
1130
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* The trust-readout tail render (mmnto-ai/totem#2327 R1–R3, R5) — replaces the
|
|
1134
|
+
* bare "N loaded" ending with the rollup, coverage denominator, why-not lines,
|
|
1135
|
+
* and the `--strict` honesty statement. Verdict lines above stay untouched.
|
|
1136
|
+
*/
|
|
1137
|
+
function renderTrustReadout(readout, ui, render) {
|
|
1138
|
+
const { log, bold } = ui;
|
|
1139
|
+
log.info(TAG, bold('── trust-readout ──'));
|
|
1140
|
+
// R1 — one vocabulary, two scopes. Units named (verdict lines ≠ contracts).
|
|
1141
|
+
log.info(TAG, `rollup (verdict lines) — global: ${renderCounts(readout.rollup.global)}`);
|
|
1142
|
+
const seats = Object.keys(readout.rollup.perSeat);
|
|
1143
|
+
if (seats.length === 0) {
|
|
1144
|
+
log.dim(TAG, 'rollup — no seat-scoped rows declared (per-seat = global)');
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
for (const seat of seats) {
|
|
1148
|
+
log.info(TAG, `rollup — seat ${render(seat)}: ${renderCounts(readout.rollup.perSeat[seat])}`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
// R2 — three counts on their own line, never one "covered" number; the
|
|
1152
|
+
// claim boundary stated in the readout's own words.
|
|
1153
|
+
const d = readout.denominator;
|
|
1154
|
+
log.info(TAG, `coverage (contracts) — ${d.mechanical} mechanically sensed · ${d.attestationOnly} attestation-only · ${d.honestAbsent} honest-absent`);
|
|
1155
|
+
log.dim(TAG, `claim boundary: ${CLAIM_BOUNDARY}`);
|
|
1156
|
+
// R3 — one why-not line per non-pass row, at the level actually probed
|
|
1157
|
+
// (green-halo cap: nothing renders above what was probed).
|
|
1158
|
+
const whyNot = readout.rows.filter((r) => r.verdict !== 'pass');
|
|
1159
|
+
if (whyNot.length > 0) {
|
|
1160
|
+
log.info(TAG, 'why-not (per non-pass row):');
|
|
1161
|
+
for (const r of whyNot) {
|
|
1162
|
+
const level = r.sensesProbed !== undefined ? ` at ${r.sensesProbed}` : '';
|
|
1163
|
+
const reason = r.reasonClass !== undefined ? ` [${r.reasonClass}]` : '';
|
|
1164
|
+
// R3 age-in-days for attestation rows. The detector message already
|
|
1165
|
+
// carries the raw date / "not recorded" text — append the derived age
|
|
1166
|
+
// only when a date exists to derive it from (no duplicate "not recorded").
|
|
1167
|
+
const age = r.reasonClass === 'attestation' && r.lastAttested !== undefined
|
|
1168
|
+
? ` · ${attestationAge(r.lastAttested)}`
|
|
1169
|
+
: '';
|
|
1170
|
+
const gate = r.skippedNotGated ? ' · blocking — skipped-not-gated, never a silent pass' : '';
|
|
1171
|
+
log.dim(TAG, ` ${render(r.lineName)}: ${r.verdict.toUpperCase()}${level}${reason} — ${render(r.message)}${age}${gate}`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
// R5 — declaredly-toothless in the readout's own words; derived from the
|
|
1175
|
+
// manifest's blocking set at run time, never from prose.
|
|
1176
|
+
const s = readout.strict;
|
|
1177
|
+
const armed = s.armed ? 'armed' : 'not armed';
|
|
1178
|
+
if (!s.gatesAnything) {
|
|
1179
|
+
log.info(TAG, `--strict (${armed}): currently gates nothing — the manifest declares no blocking: true contracts`);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
log.info(TAG, `--strict (${armed}): gates ${s.blockingIds.length} blocking contract(s): ${s.blockingIds.map(render).join(', ')}`);
|
|
811
1183
|
}
|
|
812
1184
|
}
|
|
813
1185
|
//# sourceMappingURL=doctor-parity.js.map
|