/**
 * Parity-drift sensor for `totem doctor --parity` (mmnto-ai/totem-strategy#448).
 *
 * Detection is wired across all three tractability classes (mmnto-ai/totem#2073):
 *   - **version-pinned** (PR-1, mmnto-ai/totem#2069): each deps contract whose id
 *     resolves an `@mmnto/*` package name runs through the core
 *     `detectVersionPinnedContract` engine (pin-currency verdict, local-only floor).
 *   - **mechanical content-equality** (mmnto-ai/totem#2073): three artifact shapes,
 *     all local-read-only against the running `@mmnto/cli`'s OWN in-process template:
 *       · **skills** (`claude-skills`): managed-block equality of
 *         `.claude/skills/<name>/SKILL.md` via `detectMechanicalContract`.
 *       · **git-hooks**: per-repo REGENERATED whole-file/region equality of the four
 *         `.git/hooks/*` (package-manager + tier parameterized) via
 *         `detectGeneratedArtifactContract` — catches stale-version drift (#1854).
 *       · **session-start-orientation**: STATIC whole-file equality of the
 *         `.claude/hooks/SessionStart.cjs` + `.gemini/hooks/SessionStart.js` templates,
 *         also via `detectGeneratedArtifactContract` (no parameterization).
 *   - **manual-attestation** (mmnto-ai/totem#2080): the no-mechanical-sensor class —
 *     `detectManualAttestationContract` surfaces the doctrine-currency row / vendor-SDK
 *     pin as `info` (or honest-absent `skip`), NEVER pass/warn/fail (the "never fails"
 *     contract; structurally cannot gate under `--strict`).
 *
 * The remaining contracts — the file-value-equality bot-configs (`cr-profile` etc.)
 * and the structural-presence dimensions — keep the `skip` "not yet implemented" stub;
 * their detection is a follow-on.
 *
 * The parity sensor owns its OWN render + result type (`ParityLine`) carrying a
 * WIDER status vocabulary (pass/warn/fail/info/unknown/skip) than the shared
 * `CheckStatus`, so the verdict-state split (#2073 req #1) never ripples
 * `CheckStatus` across the unrelated doctor checks.
 *
 * Sensor-not-gate: the detectors return `skip`/`warn`/`pass`/`info`/`unknown` —
 * never `fail`. The `--strict` exit-code decision lives at the CLI edge: a
 * `warn` from a `blocking: true` contract is promoted to `fail` (non-zero) ONLY
 * under `--strict`. `info`/`unknown` are never gated. Honest-absent (Tenet 14):
 * unconfigured → exactly one `skip` line; configured-but-missing / unparseable /
 * unsupported-schema → `warn`, never a crash. Dynamic-import `@mmnto/totem` to
 * keep core off the CLI cold-start graph, matching the other doctor checks.
 */
import * as path from 'node:path';
// init-templates (large canonical strings) + the node:url / node:module builtins
// are dynamic-imported inside checkParity per the packages/cli lazy-load
// guideline — see the `ok` branch.
const CHECK_NAME = 'Parity';
/**
 * Resolve the on-disk artifact(s) a mechanical contract checks, or `undefined`
 * when this slice doesn't handle the contract (hooks / presence / value-equality
 * → kept as `skip` stubs). `claude-skills` yields one artifact PER distributed
 * skill; `review-reply-skill-content` is the single review-reply skill (it
 * overlaps `claude-skills` by design — both are distinct manifest contracts on
 * the same file; flagged to strategy as a manifest observation).
 *
 * The canonical block is extracted from the running CLI's OWN `init-templates`
 * export (passed-in `extract` is the core `extractManagedBlock`, dynamic-imported
 * by the caller to keep `@mmnto/totem` off the cold-start graph) — local-read-only.
 */
function mechanicalArtifactsFor(contractId, gitRoot, extract, templates) {
    const { markers } = templates;
    switch (contractId) {
        case 'claude-skills':
            return templates.distributedSkills.map((s) => ({
                consumerPath: path.join(gitRoot, '.claude', 'skills', s.name, 'SKILL.md'),
                markers,
                canonicalBlock: extract(s.content, markers),
                lineName: `Parity: claude-skills (${s.name})`,
            }));
        case 'review-reply-skill-content':
            return [
                {
                    consumerPath: path.join(gitRoot, '.claude', 'skills', 'review-reply', 'SKILL.md'),
                    markers,
                    canonicalBlock: extract(templates.reviewReplyContent, markers),
                    lineName: 'Parity: review-reply-skill-content',
                },
            ];
        default:
            return undefined;
    }
}
// ─── Value-equality field registry (CLI-side, mmnto-ai/totem-strategy#738 Slice A) ──
/**
 * Resolve the on-disk config field(s) a `manifestation: value-equality` contract
 * checks, or `undefined` when this slice doesn't handle the id (→ a `skip` stub).
 * Each entry carries WHERE to look (file + dotted-path SEGMENTS + parse format);
 * the EXPECTED value is read by the detector from the contract's own
 * `expectedValueOrDerivation` (strategy#738 Q1 — the manifest field is the
 * canonical, no second source). Paths are pre-verified against the live cohort
 * config shapes: GCA nests its switches under a top-level `code_review:` block
 * (`code_review.pull_request_opened.*`), and `gca-on-demand` is split into two
 * independent rows per the strategy#738 Q3 ruling (each switch drifts on its own).
 */
function valueEqualityFieldsFor(contractId, gitRoot) {
    switch (contractId) {
        case 'cr-profile':
            return [
                {
                    consumerPath: path.join(gitRoot, '.coderabbit.yaml'),
                    pathSegments: ['reviews', 'profile'],
                    format: 'yaml',
                    lineName: 'Parity: cr-profile',
                },
            ];
        case 'cr-on-demand':
            return [
                {
                    consumerPath: path.join(gitRoot, '.coderabbit.yaml'),
                    pathSegments: ['reviews', 'auto_review', 'enabled'],
                    format: 'yaml',
                    lineName: 'Parity: cr-on-demand',
                },
            ];
        case 'gca-code-review':
            return [
                {
                    consumerPath: path.join(gitRoot, '.gemini', 'config.yaml'),
                    pathSegments: ['code_review', 'pull_request_opened', 'code_review'],
                    format: 'yaml',
                    lineName: 'Parity: gca-code-review',
                },
            ];
        case 'gca-summary':
            return [
                {
                    consumerPath: path.join(gitRoot, '.gemini', 'config.yaml'),
                    pathSegments: ['code_review', 'pull_request_opened', 'summary'],
                    format: 'yaml',
                    lineName: 'Parity: gca-summary',
                },
            ];
        case 'greptile-on-demand':
            return [
                {
                    consumerPath: path.join(gitRoot, 'greptile.json'),
                    pathSegments: ['skipReview'],
                    format: 'json',
                    lineName: 'Parity: greptile-on-demand',
                },
            ];
        default:
            return undefined;
    }
}
/**
 * Resolve the four git-hook artifacts the `git-hooks` contract checks, each with a
 * per-repo regenerated canonical. Checks `.git/hooks/*` only — hook-manager installs
 * (`.totem/hooks/*.sh` for husky / lefthook) are a follow-on (no cohort consumer uses
 * one today); the detector's present-without-marker → `skip` keeps a manager repo
 * honest in the meantime.
 */
function gitHookArtifactsFor(gitRoot, tier, fallbackCmd, builders) {
    const hooksDir = path.join(gitRoot, '.git', 'hooks');
    const m = builders.markers;
    return [
        {
            consumerPath: path.join(hooksDir, 'pre-commit'),
            canonicalContent: builders.buildPreCommitHook(tier),
            ownershipMarker: m.preCommit,
            lineName: 'Parity: git-hooks (pre-commit)',
        },
        {
            consumerPath: path.join(hooksDir, 'pre-push'),
            canonicalContent: builders.buildPrePushHook(fallbackCmd, tier),
            ownershipMarker: m.prePush,
            lineName: 'Parity: git-hooks (pre-push)',
        },
        {
            consumerPath: path.join(hooksDir, 'post-merge'),
            canonicalContent: builders.buildHookContent(fallbackCmd),
            ownershipMarker: m.postMerge.start,
            endMarker: m.postMerge.end,
            lineName: 'Parity: git-hooks (post-merge)',
        },
        {
            consumerPath: path.join(hooksDir, 'post-checkout'),
            canonicalContent: builders.buildPostCheckoutHookContent(fallbackCmd),
            ownershipMarker: m.postCheckout.start,
            endMarker: m.postCheckout.end,
            lineName: 'Parity: git-hooks (post-checkout)',
        },
    ];
}
/**
 * Resolve the two whole-file SessionStart hook artifacts the `session-start-orientation`
 * contract checks: `.claude/hooks/SessionStart.cjs` (canonical `CLAUDE_SESSION_START`)
 * and `.gemini/hooks/SessionStart.js` (canonical `GEMINI_SESSION_START`). Whole-file
 * static canonical, no end marker, no regeneration. A vendor file absent here is
 * honest-absent `skip` (cohort permits absence) via the detector's presence semantics;
 * a present file whose marker opens it but whose body drifted is `warn` (the orientation
 * slice generalized `isOwnedGeneratedFile` to treat a marker-at-start JS file as owned).
 */
function sessionStartArtifactsFor(gitRoot, templates) {
    return [
        {
            consumerPath: path.join(gitRoot, '.claude', 'hooks', 'SessionStart.cjs'),
            canonicalContent: templates.claude,
            ownershipMarker: templates.marker,
            lineName: 'Parity: session-start-orientation (claude)',
        },
        {
            consumerPath: path.join(gitRoot, '.gemini', 'hooks', 'SessionStart.js'),
            canonicalContent: templates.gemini,
            ownershipMarker: templates.marker,
            lineName: 'Parity: session-start-orientation (gemini)',
        },
    ];
}
/**
 * Resolve the probe(s) a capability-probe contract runs, or `undefined` when
 * this build ships no probe for the row (→ honest skip stub). Both deliverable-1
 * probes are PRESENT-rung by design: `knowledge-search-access`'s usable rung (a
 * live bounded search exec) is deliberately NOT shipped — real `totem search`
 * embeds the query via a cloud API, which a 296 §12.5 never-network probe cannot
 * run; the core detector caps the verdict at `unknown` accordingly (the
 * green-halo cap) and the rung contradiction is flagged to strategy.
 */
function capabilityProbesFor(contractId, gitRoot) {
    switch (contractId) {
        case 'knowledge-search-access':
            return [
                {
                    kind: 'mcp-registration',
                    consumerPath: path.join(gitRoot, '.mcp.json'),
                    probedLevel: 'present',
                    lineName: 'Parity: knowledge-search-access',
                },
            ];
        case 'claude-settings-minimum-capability':
            return [
                {
                    kind: 'settings-floor',
                    consumerPath: path.join(gitRoot, '.claude', 'settings.json'),
                    mcpJsonPath: path.join(gitRoot, '.mcp.json'),
                    probedLevel: 'present',
                    lineName: 'Parity: claude-settings-minimum-capability',
                },
            ];
        default:
            return undefined;
    }
}
/**
 * Resolve the running `@mmnto/cli`'s version + install path for the req-#5
 * binary self-report (the Stale-Doctor-Paradox guard — surface WHICH binary
 * computed the skills verdict so a shadowed/stale global is visible). Best-effort:
 * a resolution failure degrades the self-report to absent (the verdict still
 * renders, just without provenance), never a crash. Resolves from THIS module's
 * own URL, so it names the actual running install (workspace dist vs a
 * node_modules vendor vs a global shadow).
 */
function resolveRunningBinary(urlMod, createRequireFn) {
    try {
        const here = urlMod.fileURLToPath(import.meta.url);
        // dist/commands/doctor-parity.js → up two → the @mmnto/cli package root.
        const cliRoot = path.resolve(path.dirname(here), '..', '..');
        const req = createRequireFn(import.meta.url);
        const pkg = req('../../package.json');
        if (typeof pkg.version !== 'string')
            return undefined;
        return { version: pkg.version, path: cliRoot };
        // totem-context: best-effort cosmetic read — the binary self-report degrades to absent on ANY resolution failure and NEVER re-throws (GCA review), so a shadowed / broken install can't crash the doctor pipeline (Tenet 13: a cosmetic read must not break the sensor).
    }
    catch {
        return undefined;
    }
}
/**
 * Resolve, parse, and report the parity manifest as `ParityLine`s.
 *
 * Returns `{ results, blockingDriftIds }`: `results[0]` is always the section
 * summary line; in the `ok` path it is followed by one line per contract (or
 * per artifact, for multi-artifact mechanical contracts) — a pin-currency
 * verdict for the deps version-pinned contracts, a content-equality verdict for
 * the mechanical skills contracts, and a `skip` stub for everything else. All
 * non-`ok` paths return a single summary entry and an empty `blockingDriftIds`.
 *
 * @param cwd The directory to resolve config + manifest against (config/repo root).
 */
export async function checkParity(cwd) {
    const { loadConfig, resolveConfigPath, isGlobalConfigPath } = await import('../utils.js');
    const { deriveCohortRepoId, detectCapabilityProbeContract, detectGeneratedArtifactContract, detectManualAttestationContract, detectMechanicalContract, detectValueEqualityContract, detectVersionPinnedContract, extractManagedBlock, loadParityManifest, packageNameForContract, PARITY_MANIFESTATIONS, resolveGitRoot, SUPPORTED_PARITY_SCHEMA_VERSION, TOOLCHAIN_DIMENSION, } = await import('@mmnto/totem');
    // Read the config best-effort: a missing/corrupt config is the honest-absent
    // path (no parity manifest configured), not a crash. Mirrors the config-load
    // fallback in doctorCommand — surface only on a defective error object so
    // sentinels still propagate.
    let configValue;
    // Relative manifest paths anchor at the config's OWN directory, not the
    // invocation cwd, so the field resolves consistently no matter which subdir
    // the doctor runs from. resolveConfigPath only checks cwd + the global profile
    // today (no upward walk), so this equals cwd for the local case — the explicit
    // anchor just keeps it correct if resolution ever changes.
    let manifestRoot = cwd;
    // Tier the git hooks were generated at. Resolved the SAME way `hooksCommand` does
    // (config.hooks.tier, default 'standard') so the doctor regenerates the canonical
    // at the CONFIGURED tier — a hook on disk that does not match its repo's configured
    // tier is genuine drift, which the content compare correctly surfaces.
    let hookTier = 'standard';
    try {
        const configPath = resolveConfigPath(cwd);
        // Repo-scoped by design: the manifest location is per-repo, so a config-less
        // repo that only resolves the GLOBAL ~/.totem profile is honest-absent for
        // parity. Never leak a global orient.parityManifest into a repo-less result
        // (that would make the sensor machine-dependent) — only a repo-local config
        // contributes the field.
        if (isGlobalConfigPath(configPath)) {
            configValue = undefined;
        }
        else {
            manifestRoot = path.dirname(configPath);
            const config = await loadConfig(configPath);
            configValue = config.orient?.parityManifest;
            hookTier = config.hooks?.tier ?? 'standard';
        }
        // totem-context: a missing/corrupt totem config is the honest-absent path (treated as "no parity manifest configured"), not a sensor failure — the doctor runs against config-less repos by design.
    }
    catch (err) {
        if (err instanceof Error && err.message.length === 0) {
            throw err;
        }
        configValue = undefined;
    }
    // Single source of truth for "is parity configured here" — derived from the
    // SAME resolution (incl. the isGlobalConfigPath guard above) so the CLI edge
    // never re-derives config and never leaks a global manifest into the fold.
    const configured = configValue !== undefined;
    const result = loadParityManifest(configValue, manifestRoot);
    switch (result.status) {
        case 'not-configured':
            // Honest-absent: exactly one skip line. Not a failure.
            return single({
                name: CHECK_NAME,
                status: 'skip',
                message: 'no parity manifest configured',
            }, configured);
        case 'not-found': {
            // mmnto-ai/totem#2094 — when the configured manifest path lives under
            // node_modules/ (the strategy-doctrine optional-pin shape), the normal
            // unauthed-CI state is "pin not installed" (optional deps skipped without
            // npm read-auth), NOT a misconfigured path. Name the install-side cause so
            // the hint stops misdiagnosing the expected CI condition as a config error.
            // Match node_modules as a path segment at the start, middle, or end of the
            // path — a bare/relative `node_modules` or one with no trailing separator
            // still counts (greptile #2252). In that state the config is presumed
            // correct (the pin path is right), so the remediation is install-focused,
            // not "fix your config" (coderabbit #2252).
            const underNodeModules = /(?:^|[\\/])node_modules(?:[\\/]|$)/.test(result.path);
            const remediation = underNodeModules
                ? 'Install the pin dependency (npm read-auth required; optional deps are skipped in unauthed installs).'
                : 'Fix orient.parityManifest in your totem config to point at the manifest.';
            return single({
                name: CHECK_NAME,
                status: 'warn',
                message: `parity manifest not found at ${rel(cwd, result.path)}`,
                remediation,
            }, configured);
        }
        case 'unparseable':
            return single({
                name: CHECK_NAME,
                status: 'warn',
                message: `parity manifest unreadable at ${rel(cwd, result.path)}: ${result.reason}`,
                remediation: 'Fix the manifest YAML / schema, then re-run totem doctor --parity.',
            }, configured);
        case 'unsupported-schema':
            return single({
                name: CHECK_NAME,
                status: 'warn',
                message: `parity manifest schema v${result.schemaVersion} unsupported (this doctor supports v${SUPPORTED_PARITY_SCHEMA_VERSION})`,
                remediation: 'Upgrade @mmnto/cli or align the manifest schema-version.',
            }, configured);
        case 'ok': {
            const { contracts } = result.manifest;
            const summary = {
                name: CHECK_NAME,
                status: 'pass',
                message: `parity manifest: ${contracts.length} contract(s) loaded`,
            };
            // Lazy-load init-templates (large canonical strings) + the node builtins
            // only on the ok path — the honest-absent cases above never pay for them
            // (packages/cli lazy-load guideline).
            const { createRequire } = await import('node:module');
            const url = await import('node:url');
            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');
            const skillTemplates = {
                distributedSkills: DISTRIBUTED_CLAUDE_SKILLS,
                reviewReplyContent: REVIEW_REPLY_SKILL_CONTENT,
                markers: { start: SKILL_MARKER_START, end: SKILL_MARKER_END },
            };
            // The running CLI's own whole-file SessionStart hook templates (static — no
            // parameterization), lazy-loaded on the ok path only (mmnto-ai/totem#2073 orientation slice).
            const sessionStartTemplates = {
                claude: CLAUDE_SESSION_START,
                gemini: GEMINI_SESSION_START,
                marker: SESSION_START_MARKER,
            };
            // The running CLI's own hook generators + markers + package-manager probe,
            // lazy-loaded on the ok path only (cold-start guideline). The canonical for
            // each git hook is regenerated per-repo from these (mmnto-ai/totem#2073 hooks slice).
            const { buildPreCommitHook, buildPrePushHook, buildHookContent, buildPostCheckoutHookContent, getFallbackCommand, TOTEM_PRECOMMIT_MARKER, TOTEM_PREPUSH_MARKER, TOTEM_HOOK_MARKER, TOTEM_HOOK_END, TOTEM_CHECKOUT_MARKER, TOTEM_CHECKOUT_END, } = await import('./install-hooks.js');
            const hookBuilders = {
                buildPreCommitHook,
                buildPrePushHook,
                buildHookContent,
                buildPostCheckoutHookContent,
                markers: {
                    preCommit: TOTEM_PRECOMMIT_MARKER,
                    prePush: TOTEM_PREPUSH_MARKER,
                    postMerge: { start: TOTEM_HOOK_MARKER, end: TOTEM_HOOK_END },
                    postCheckout: { start: TOTEM_CHECKOUT_MARKER, end: TOTEM_CHECKOUT_END },
                },
            };
            // Shared detection context. The cohort floor + repoId derive from the git
            // root (anchored there, not the deep cwd — mirrors the core resolver).
            // resolveGitRoot returns null outside a repo; fall back to cwd so the
            // local self-in-tree / sibling probes still have an anchor.
            const gitRoot = safeGitRoot(resolveGitRoot, cwd) ?? cwd;
            const repoId = deriveCohortRepoId(cwd, { gitRoot });
            const binary = resolveRunningBinary(url, createRequire);
            // Derived from the repo's lockfile (the SAME probe the installer uses), so the
            // regenerated git-hook canonical matches THIS repo's package manager — no false drift.
            const fallbackCmd = getFallbackCommand(gitRoot);
            const blockingDriftIds = [];
            // The consumers-scope guard shared by the routing branches that don't
            // self-guard in core (mechanical + capability-probe): a scoped contract
            // must not emit drift in a repo that is not an intended consumer.
            const consumersSkip = (c) => {
                if (c.consumers === undefined)
                    return undefined;
                if (repoId === undefined) {
                    return [
                        lineFor(`Parity: ${c.id}`, {
                            status: 'skip',
                            message: `cannot determine applicability — repo id unresolvable; contract is scoped to consumers [${c.consumers.join(', ')}]`,
                        }),
                    ];
                }
                if (!c.consumers.includes(repoId)) {
                    return [
                        lineFor(`Parity: ${c.id}`, {
                            status: 'skip',
                            message: `cohort permits absence here (${repoId} not in consumers)`,
                        }),
                    ];
                }
                return undefined;
            };
            // flatMap, not map: a mechanical contract (claude-skills) expands to one
            // line PER distributed skill, so the per-contract count can exceed the
            // contract count.
            const perContract = contracts.flatMap((c) => {
                // ── manifestation routing (promoted field, mmnto-ai/totem#2140) ──
                // capability-probe rows route BEFORE the tractability dispatch: both
                // deliverable-1 rows are `tractability: mechanical` (Green-decidable)
                // but sense via probes, not artifact content-equality. Recognized
                // NON-probe rungs (managed-block, version-pin, …) are informational —
                // they fall through to the existing tractability routing unchanged.
                if (c.manifestation === 'capability-probe') {
                    const scopeSkip = consumersSkip(c);
                    if (scopeSkip !== undefined)
                        return scopeSkip;
                    const probes = capabilityProbesFor(c.id, gitRoot);
                    if (probes === undefined) {
                        return [
                            stub(c, `${c.dimension} (capability-probe) — probe not yet implemented for this row`),
                        ];
                    }
                    let blockingDrift = false;
                    const lines = probes.map((p) => {
                        const verdict = detectCapabilityProbeContract({
                            kind: p.kind,
                            consumerPath: p.consumerPath,
                            probedLevel: p.probedLevel,
                            ...(p.mcpJsonPath !== undefined ? { mcpJsonPath: p.mcpJsonPath } : {}),
                            ...(c.senses !== undefined ? { declaredSenses: c.senses } : {}),
                        });
                        if (verdict.status === 'warn' && c.blocking === true)
                            blockingDrift = true;
                        return lineFor(p.lineName, verdict);
                    });
                    if (blockingDrift)
                        blockingDriftIds.push(c.id);
                    return lines;
                }
                // ── value-equality routing (mmnto-ai/totem-strategy#738 Slice A) ──
                // Routes on `manifestation` BEFORE the tractability dispatch (same as
                // capability-probe): these bot-review-config rows are `tractability:
                // mechanical` but sense a SCALAR at a dotted path, not artifact content-
                // equality. The expected value is the contract's own
                // `expectedValueOrDerivation` (the canonical); the registry supplies only
                // the file + path + format. No `consumersSkip` here — like
                // detectVersionPinnedContract, detectValueEqualityContract SELF-guards the
                // consumers scope in core (greptile review on #2249); a second CLI guard
                // would be the dead-letter / message-divergence risk consumersSkip is
                // documented to avoid.
                if (c.manifestation === 'value-equality') {
                    const fields = valueEqualityFieldsFor(c.id, gitRoot);
                    if (fields === undefined) {
                        return [
                            stub(c, `${c.dimension} (value-equality) — drift detection not yet implemented for this row`),
                        ];
                    }
                    // A row maps to ≥1 field (the gca two-row split shares one file); tag the
                    // contract id at most ONCE for --strict so the count reflects contracts.
                    let blockingDrift = false;
                    const lines = fields.map((field) => {
                        const verdict = detectValueEqualityContract(c, { repoId, field });
                        if (verdict.status === 'warn' && c.blocking === true)
                            blockingDrift = true;
                        return lineFor(field.lineName, verdict);
                    });
                    if (blockingDrift)
                        blockingDriftIds.push(c.id);
                    return lines;
                }
                if (c.manifestation !== undefined &&
                    !PARITY_MANIFESTATIONS.includes(c.manifestation)) {
                    // Fail-loud PER ROW, never per manifest (the total-outage guard): an
                    // unrecognized rung value surfaces verbatim instead of darking the
                    // sensor or silently mis-routing.
                    return [
                        stub(c, `manifestation '${c.manifestation}' unrecognized by this doctor — drift detection not yet implemented`),
                    ];
                }
                // ── version-pinned deps (PR-1) ──
                if (c.tractability === 'version-pinned') {
                    const packageName = packageNameForContract(c, gitRoot);
                    // A toolchain-version row with no deps package pins its engine via the
                    // consumer's `packageManager` field; detectVersionPinnedContract
                    // self-routes those to the toolchain reader (mmnto-ai/totem#2115). Only
                    // stub a version-pinned row that's neither a deps package nor a toolchain.
                    if (packageName === undefined && c.dimension !== TOOLCHAIN_DIMENSION) {
                        return [
                            stub(c, `${c.dimension} (version-pinned) — drift detection not yet implemented`),
                        ];
                    }
                    const verdict = detectVersionPinnedContract(c, { cwd, gitRoot, repoId, packageName });
                    if (verdict.status === 'warn' && c.blocking === true)
                        blockingDriftIds.push(c.id);
                    return [verdictToLine(c, verdict)];
                }
                // ── mechanical content-equality (mmnto-ai/totem#2073) ──
                if (c.tractability === 'mechanical') {
                    // Honor the contract's `consumers` scope before sensing drift (mirrors
                    // detectVersionPinnedContract, which self-guards). A scoped mechanical
                    // contract must not emit drift — or, under --strict, fail — in a repo that
                    // is not an intended consumer (CodeRabbit review on mmnto-ai/totem#2079).
                    const scopeSkip = consumersSkip(c);
                    if (scopeSkip !== undefined)
                        return scopeSkip;
                    // Generated-artifact contracts (whole-file / region content-equality): the git
                    // hooks (regenerated per-repo for this package manager + tier — catches the
                    // stale-version drift half of mmnto-ai/totem#1854) and the static whole-file
                    // SessionStart hooks (orientation slice). Both resolve a GeneratedArtifact[] and
                    // run the same presence-aware detector + once-per-contract blocking tag.
                    let generatedArtifacts;
                    // Artifact-class copy threaded into the detector so the absence/drift
                    // remediation names the RIGHT installer per class (Greptile review on
                    // mmnto-ai/totem#2082): git hooks → `totem hook install`; the static
                    // SessionStart hooks → `totem init`.
                    let artifactLabel = 'artifact';
                    let installCommand = 'totem init';
                    if (c.id === 'git-hooks') {
                        generatedArtifacts = gitHookArtifactsFor(gitRoot, hookTier, fallbackCmd, hookBuilders);
                        artifactLabel = 'git hook';
                        installCommand = 'totem hook install';
                    }
                    else if (c.id === 'session-start-orientation') {
                        generatedArtifacts = sessionStartArtifactsFor(gitRoot, sessionStartTemplates);
                        artifactLabel = 'SessionStart hook';
                        installCommand = 'totem init';
                    }
                    if (generatedArtifacts !== undefined) {
                        // A drift on any artifact tags the contract id at most ONCE so the --strict
                        // count reflects contracts, not artifacts (mirrors the skills branch).
                        let blockingDrift = false;
                        const lines = generatedArtifacts.map((a) => {
                            const verdict = detectGeneratedArtifactContract({
                                canonicalContent: a.canonicalContent,
                                consumerPath: a.consumerPath,
                                ownershipMarker: a.ownershipMarker,
                                artifactLabel,
                                installCommand,
                                ...(a.endMarker !== undefined ? { endMarker: a.endMarker } : {}),
                                ...(binary !== undefined ? { binary } : {}),
                            });
                            if (verdict.status === 'warn' && c.blocking === true)
                                blockingDrift = true;
                            return lineFor(a.lineName, verdict);
                        });
                        if (blockingDrift)
                            blockingDriftIds.push(c.id);
                        return lines;
                    }
                    // skills: managed-block content equality against the in-process template.
                    const artifacts = mechanicalArtifactsFor(c.id, gitRoot, extractManagedBlock, skillTemplates);
                    if (artifacts === undefined) {
                        return [
                            stub(c, `${c.dimension} (mechanical) — drift detection not yet implemented for this sub-class`),
                        ];
                    }
                    // A multi-artifact contract (claude-skills) can drift on several
                    // artifacts; tag the contract id at most ONCE so the --strict count
                    // reflects contracts, not artifacts.
                    let blockingDrift = false;
                    const lines = artifacts.map((a) => {
                        const verdict = detectMechanicalContract({
                            canonicalBlock: a.canonicalBlock,
                            consumerPath: a.consumerPath,
                            markers: a.markers,
                            ...(binary !== undefined ? { binary } : {}),
                        });
                        if (verdict.status === 'warn' && c.blocking === true)
                            blockingDrift = true;
                        return lineFor(a.lineName, verdict);
                    });
                    if (blockingDrift)
                        blockingDriftIds.push(c.id);
                    return lines;
                }
                // ── manual-attestation (mmnto-ai/totem#2073 manual-attestation slice) ──
                // The claim-class with no mechanical sensor: the detector surfaces the
                // tracked vendor-SDK pin (package set) or doctrine-currency row (no package)
                // as `info`, or honest-absent `skip` — NEVER pass/warn/fail/unknown. An
                // `info` can't enter blockingDriftIds, so these contracts cannot fail even
                // under --strict (the manifest's "never fails" contract).
                if (c.tractability === 'manual-attestation') {
                    // The detector reads the sub-class discriminant (`c.package`) + the
                    // canonical source directly off the contract. `package:` set ⇒ vendor-SDK
                    // pin read; unset ⇒ doctrine-row pure-info surface. `attested` carries the
                    // manifest's `last-attested:` date when present (strategy#540 /
                    // mmnto-ai/totem#2125) — message refinement only, never a verdict input.
                    const verdict = detectManualAttestationContract(c, {
                        cwd,
                        repoId,
                        ...(c.lastAttested !== undefined ? { attested: c.lastAttested } : {}),
                    });
                    return [verdictToLine(c, verdict)];
                }
                // ── anything else (a future tractability) → skip stub; the slice boundary stays observable ──
                return [
                    stub(c, `${c.dimension} (${c.tractability}) — drift detection not yet implemented`),
                ];
            });
            return { results: [summary, ...perContract], blockingDriftIds, configured };
        }
    }
}
/** Wrap a single summary line in the `ParityCheckResult` shape (no blocking ids). */
function single(result, configured) {
    return { results: [result], blockingDriftIds: [], configured };
}
/** Map a core `ParityContractVerdict` to a `ParityLine` keyed by the contract id. */
function verdictToLine(contract, verdict) {
    return lineFor(`Parity: ${contract.id}`, verdict);
}
/** Build a `ParityLine` from an explicit display name + a core verdict. */
function lineFor(name, verdict) {
    return {
        name,
        status: verdict.status,
        message: verdict.message,
        ...(verdict.remediation !== undefined ? { remediation: verdict.remediation } : {}),
    };
}
/** The `skip` "not yet implemented" stub for a contract this build doesn't sense. */
function stub(contract, message) {
    return { name: `Parity: ${contract.id}`, status: 'skip', message };
}
/**
 * Resolve the git root, swallowing the `TotemGitError` that `resolveGitRoot`
 * throws on a git hiccup (permission error / corrupted index) — the parity
 * sensor degrades to the cwd anchor rather than crashing the doctor pipeline.
 */
function safeGitRoot(resolve, cwd) {
    try {
        return resolve(cwd);
        // totem-context: resolveGitRoot throws on permission errors / corrupted index; the parity sensor degrades to a cwd anchor rather than crashing — a git hiccup must not sink the doctor pipeline.
    }
    catch {
        return null;
    }
}
/** Repo-root-relative display path; falls back to the absolute path. */
function rel(cwd, target) {
    const r = path.relative(cwd, target);
    return r.length > 0 ? r : target;
}
// ─── CLI entry ──────────────────────────────────────────
// Same value as CHECK_NAME — aliased (not re-literal'd) so the two can't drift.
const TAG = CHECK_NAME;
/**
 * CLI entry — runs `checkParity`, renders each `ParityLine`, and throws a
 * `TotemError` when a blocking contract drifted under `--strict` so the
 * top-level `handleError` produces the non-zero exit code (no direct
 * `process.exit` per AGENTS.md).
 */
export async function doctorParityCliCommand(options = {}) {
    const { TotemError, sanitizeForTerminal } = await import('@mmnto/totem');
    const { bold, errorColor, log, success: successColor, warn: warnColor, } = await import('../ui.js');
    // Manifest-derived text (paths, parse reasons, contract metadata) is sourced
    // from repo-controlled files; sanitize + flatten before logging so embedded
    // ANSI / newlines can't forge extra doctor lines (matches checkStrategyRoot
    // in doctor.ts).
    const render = (text) => sanitizeForTerminal(text)
        .replace(/[\t\n]+/g, ' ')
        .replace(/ {2,}/g, ' ')
        .trim();
    const cwd = options.cwdForTest ?? process.cwd();
    const { results, blockingDriftIds, configured } = await checkParity(cwd);
    // Folded-into-`--strict` no-op: when this run is the strict fold (not an explicit
    // `doctor --parity`) and no repo-local manifest is configured, render and gate
    // nothing — a non-adopter's `doctor --strict` stays byte-identical to before the
    // fold. Explicit `--parity` (onlyWhenConfigured omitted) still shows the SKIP.
    if (options.onlyWhenConfigured && !configured)
        return;
    // Under --strict, a blocking contract's drift `warn` is rendered + gated as a
    // FAIL. A contract's drift can render across MULTIPLE artifact lines
    // (e.g. `Parity: claude-skills (signoff)`), so a line is promotable when its
    // name is the `Parity: <id>` summary OR a `Parity: <id> (…)` artifact line —
    // an exact-name Set would leave the artifact lines rendered as WARN while the
    // command still exits non-zero (GCA review on the PR). The trailing space
    // guards against a contract id that is a prefix of another.
    const isPromotable = (name) => blockingDriftIds.some((id) => name === `Parity: ${id}` || name.startsWith(`Parity: ${id} `));
    for (const r of results) {
        const status = options.strict && r.status === 'warn' && isPromotable(r.name) ? 'fail' : r.status;
        switch (status) {
            case 'pass':
                log.success(TAG, `${successColor(bold('PASS'))} — ${render(r.message)}`);
                break;
            case 'warn':
                log.warn(TAG, `${warnColor(bold('WARN'))} — ${render(r.message)}`);
                if (r.remediation)
                    log.dim(TAG, `→ ${render(r.remediation)}`);
                break;
            case 'info':
                // An attested, intentional fork — neutral/informational, never gated.
                log.info(TAG, `${bold('INFO')} — ${render(r.message)}`);
                if (r.remediation)
                    log.dim(TAG, `→ ${render(r.remediation)}`);
                break;
            case 'unknown':
                // The Stale-Doctor-Paradox state: the canonical was unresolvable, so the
                // verdict is unprovable (NOT a pass — no self-certification). A caution,
                // not drift; never gated.
                log.warn(TAG, `${warnColor(bold('UNKNOWN'))} — ${render(r.message)}`);
                if (r.remediation)
                    log.dim(TAG, `→ ${render(r.remediation)}`);
                break;
            case 'fail':
                // Mandated 'Totem Error' tag (packages/cli convention) — marks internal
                // error output, distinct from the contextual TAG used for pass/warn/skip.
                log.error('Totem Error', `${errorColor(bold('FAIL'))} — ${render(r.message)}`);
                if (r.remediation)
                    log.dim(TAG, `→ ${render(r.remediation)}`);
                break;
            case 'skip':
                log.dim(TAG, `SKIP — ${render(r.message)}`);
                break;
        }
    }
    // Sensor-not-gate: drift is report-only by default (exit 0). Only `--strict`
    // promotes a `blocking: true` contract's drift to a non-zero exit; non-blocking
    // drift never gates, and `info`/`unknown` are never promoted. The detectors
    // emit no raw `fail` status, so the gate is purely the strict+blocking
    // promotion — the gating model for a future slice that DOES emit a `fail` is
    // settled when that slice lands (CR review mmnto-ai/totem#2071: keep the gate from
    // suggesting a non-strict path that would break sensor-not-gate).
    if (options.strict && blockingDriftIds.length > 0) {
        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.');
    }
}
//# sourceMappingURL=doctor-parity.js.map