@dzhechkov/harness-cli 0.8.9 → 0.8.10
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/.dz-manifest.json +8 -8
- package/README.md +27 -6
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +598 -111
- package/dist/cli.js.map +1 -1
- package/package.json +9 -9
- package/sbom.json +7 -7
- package/src/cli.ts +647 -64
package/dist/cli.js
CHANGED
|
@@ -12,22 +12,22 @@ import { isBooleanFlag } from './boolean-flags.js';
|
|
|
12
12
|
import { resolveInstallSpec } from './install-spec.js';
|
|
13
13
|
import { execFile, execFileSync, execSync, spawn, spawnSync } from 'node:child_process';
|
|
14
14
|
import { createHash, randomBytes } from 'node:crypto';
|
|
15
|
-
import { homedir, tmpdir } from 'node:os';
|
|
15
|
+
import { homedir, hostname, tmpdir } from 'node:os';
|
|
16
16
|
import { createRequire } from 'node:module';
|
|
17
17
|
import { isDeepStrictEqual } from 'node:util';
|
|
18
|
-
import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, discoverSkillIds, runIntegrationsVerify, resolvePackageSkillRoots, PACKAGE_SKILL_LAYOUTS, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, discoverVerifiablePackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, resolveCodexHome, withNamedLockSync,
|
|
18
|
+
import { createSkill, getSkillInfo, listSkillsDetailed, formatSkillLoadFailures, formatSkillApplyFailures, resolveTargetName, formatTargetProblem, formatTargetAliasNote, TARGET_NAMES_SORTED, runDoctor, runInit, discoverSkillIds, resolveSelection, formatSelectRefusal, runIntegrationsVerify, resolvePackageSkillRoots, PACKAGE_SKILL_LAYOUTS, benchmarkSkill, benchmarkSkills, scanMcp, reconcileCapabilities, RECONCILE_BANNER, buildRegistry, discoverSkillPackDirs, discoverVerifiablePackDirs, checkUpstream, compareSkills, checkAllUpstream, sweepSkillDrift, syncCanonicalSkill, checkUpgrades, discoverPackages, discoverSourcePackages, fetchAllDownloads, filterByCategory, pretrain, recommend, generatePlugin, publishPackages, runSetup, runMigrate, searchRegistry, runSync, runVerify, runInitAgentsMd, runInitGeminiMd, runSyncAgentsPolicy, runSyncCodexHooks, resolveCodexHome, withNamedLockSync,
|
|
19
19
|
// dz workflow run (feature dz-workflow-run): the pure scheduler + the dispatch adapters.
|
|
20
|
-
TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher, makeCodexExecDispatcher, NamedLockTimeoutError, NamedLockCompromisedError, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, downgradeForStaleEvidence, findStaleTranscriptEvidence, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, deriveAttestation, stampAttestation, corroborate, NOT_WITNESSED, renderTimelineHtml, importEcc, recordPattern, recordLessonForms, normalizeLessonForms, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, CHECKPOINT_STAGES, estimateEta, extractStageSamples, formatEta, parseCheckpointLines, segmentRun, computeUsage, deriveCostLedger, planLedgerBackfill, listCostLedgerRuns, resolveLedgerRunId, AMBIGUOUS, stampCheckpointLine, LEDGER_FILL_SOURCE, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, parseWeeklyResetAnchor, claimCheck, summarize, BUNDLED_SLOP_REGISTRY_URL, DEFAULT_SLOP_CONFIG, parseSlopRegistry, slopLint, validateSlopLintConfig, queryBookKnowledge, loadStorePatternsSync, patternRecordId, patternIdentityOf, mergeLessonMatchedForms, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, bookKbPath, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, EVENT_CHAIN_TAIL_BYTES, EMPTY_LOG_TAIL, readTailInfo, appendChainedLines, verifyEventChainText, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
|
|
20
|
+
TRACE_RUNID_RE, WF_RUN_OWNER_HOST, preflight, runWorkflow, makeClaudePDispatcher, makeCodexExecDispatcher, NamedLockTimeoutError, NamedLockCompromisedError, POLICY_SOURCES, detectPolicyDrift, hasPolicyFence, TARGET_NAMES, buildParityMatrix, downgradeForStaleEvidence, findStaleTranscriptEvidence, TARGET_CAPABILITIES, TARGET_SHORT_LABELS, WORKFLOW_TEMPLATES_RETIRED_MESSAGE, parsePlan, isParseErrors, validatePlan, normalizePlan, planDigest, toTraceProjection, renderPlan, mergeRender, lint, lintExitCode, LOOP_BLOBS, parseTrace, assembleTimeline, runInvariants, deriveAttestation, stampAttestation, corroborate, NOT_WITNESSED, renderTimelineHtml, importEcc, recordPattern, recordLessonForms, normalizeLessonForms, resolveLearningBackend, storeStats, consolidateSessions, pruneNoisePatterns, lessonDeltaReport, removePatternsByIds, snapshotStore, recallHybrid, teachGuard, mirrorPatternsToVector, mirrorEntriesToVector, patternVectorEntry, readMemoryLearningConfig, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns, clearAgentdbQuarantine, vectorMirrorEnabled, vectorTierStatus, resolveVectorEngine, reindexVectorStore, harmonizeVectorStore, importRvfCheckpoint, statuslineData, writeFeatureAdrState, CHECKPOINT_STAGES, estimateEta, extractStageSamples, formatEta, parseCheckpointLines, segmentRun, computeUsage, deriveCostLedger, planLedgerBackfill, listCostLedgerRuns, resolveLedgerRunId, AMBIGUOUS, stampCheckpointLine, LEDGER_FILL_SOURCE, renderCostLedger, verifyCostLedgerReport, writeCostLedgerJsonl, COST_LEDGER_SCOPE, deriveUsageCalibration, normalizeClaudeUsageModelKey, readUsageLimits, parseWeeklyResetAnchor, claimCheck, summarize, BUNDLED_SLOP_REGISTRY_URL, DEFAULT_SLOP_CONFIG, parseSlopRegistry, slopLint, validateSlopLintConfig, queryBookKnowledge, loadStorePatternsSync, patternRecordId, patternIdentityOf, mergeLessonMatchedForms, loadStoreRecords, recordToPattern, bundleSkills, brainHome, listBrain, bookKbPath, promoteProjectToBrain, updateBrainSource, queryBrain, groundPrompt, expandKu, reindexBrainVectors, buildPrimer, exportBrainSlice, importBrainSlice, registerKusToBrain, RECALL_USAGE_LOG_RELATIVE, RECALL_USAGE_LOG_MAX_BYTES, parseRecallUsageLog, buildRecallUsageReport, EVENT_CHAIN_TAIL_BYTES, EMPTY_LOG_TAIL, readTailInfo, appendChainedLines, verifyEventChainText, classifyChainDefects, CHAINED_JOURNALS, buildManifest, buildSbom, resolveTrustRoot, decideVerifyPolicy, generateSigningKeypair, evaluateGuard, resolveRules, auditRecord, guardExitCode, DEFAULT_RULES, parsePnpmLockImporters, scannableStubPath,
|
|
21
21
|
// guard-promotion (feature guard-promotion, scout idea #1)
|
|
22
22
|
assembleCandidates, renderPromotionReport, renderPromotionAdr, normalizePromotionState, nextPromotionState, recordPromotionRunEvidence, isLessonRuleContentAnchor, isOffsetIsoTimestamp, globMatch, promotionAdrRelPath, DEFAULT_WINDOW_DAYS, DEFAULT_PERIODS, MAX_CONTENT_FETCHES, BUILTIN_COVERAGE, decideProvenance, isInsideTree, signManifest, verifyManifest, listSignablePackFiles, assertKeyOutsideTree, decidePublishGate, collectPackageFacts, planReleaseGates, selectAffectedPackages, classifyGateExecutions, buildFailureIssue, buildReleaseNotes, releaseTagName, firstOutputLine, formatPublishError, MANIFEST_NAME, SBOM_NAME, buildArchitectureMap, renderMapHuman, findArchitectureDrift, renderDriftReport, scanWorkspacePackages, loadSubsystemManifest, loadProductVision, checkFeatureAgainstArchitecture, renderArchCheck, planProjectSkills, guidanceForStage, renderInjectionReport, analyzeCorpus, renderRakeReport, renderCriticSection, rakeAsLesson, rakeReward, DEFAULT_RAKE_THRESHOLDS, streamSessionEvents, findLatestTranscript, detectProcessRakes, buildRetro, renderRetro, retroLessonText, PROCESS_SIGNATURES, RETRO_DOMAIN, scanForSetup, buildSetupPlan, scaffoldFromSpec, renderScaffoldPreview, readExistingForScaffold, assembleChallengeContext, buildChallengeBrief, planDiscriminationCheck, classifyDiscrimination, classifyExecutionEvidence, pickAdversaryModel, CHALLENGE_QUESTIONS, loadOutcomes, renderOutcomes, statsForKey, selectAutoCost, recordProvisional, finalizeOutcome, harvestStageOutcomes, recommendModels, planFeed, unfedRuns, GRADE_SUCCESS_FLOOR, COST_LADDER, splitScenarios, budgetPlan, selectWinner, proseScopeOk, renderProseDiff, readScenarioIds, DEFAULT_MAX_JUDGE_RUNS, collectDeliveryFacts, planDeliveryCheck, renderDeliveryBrief, classifyDelivery, isUsablePlaneResult, renderDeliveryReview, scanSkillsLayout, declaredPluginSurface, parseInitFacts, verifyRegistration, buildContentProbePrompt, classifyContentProbe, renderContentProbe, findNonRegistrableSkillDirs, assembleCompoundingReport, buildDeadwoodReport, compactCmdUsageIfNeeded, measureCmdUsageDepthDays, recordCommandInvocation, resolveCmdUsageRoot, renderDeadwoodReport, CMD_USAGE_LOG_RELATIVE, banditStats, narrowBanditReport, renderBanditHealth,
|
|
23
23
|
// Cold-vs-warm EPOCH RUNNER (feature epoch-replay) — orchestrates + scores, never calls a model.
|
|
24
|
-
replayableInstances, buildWorkOrder, buildJudgePrompts, unblindJudgments, verifyWorkOrder, isValidMargin, DIGEST_HONEST_SCOPE, scoreEpochReplay, generateMockOutcomes, renderEpochReplayResult, renderWorkOrderSummary, renderJudgePromptsSummary, WORK_ORDER_KIND, DEFAULT_MOCK_N, DEFAULT_MOCK_SEED, scoreRun, readQeGrade, recapWindow, decideHorizon, withinWindow, buildRecap, renderRecap, parseSourceManifest, tgPostHtmlIssues, tgVisibleLength, decideTgSend, TG_TEXT_LIMIT, countRecallEventsForRun, unknownFlagNotice, mirrorWriterExplanation, appendRecallUsage, closenessLine, anyAboveFloor, decideNameCheck, renderNameCheck, exportedNamesIn, dispatchedCommandsIn, decideSourceProvenance, renderSourceProvenance, REFUSED_HORIZONS, renderScorecard, renderCompoundingReport, readReinforcementState, readQuarantineState, registrationExitCode, renderRegistrationReport,
|
|
24
|
+
replayableInstances, buildWorkOrder, buildJudgePrompts, unblindJudgments, verifyWorkOrder, isValidMargin, DIGEST_HONEST_SCOPE, scoreEpochReplay, generateMockOutcomes, renderEpochReplayResult, renderWorkOrderSummary, renderJudgePromptsSummary, WORK_ORDER_KIND, DEFAULT_MOCK_N, DEFAULT_MOCK_SEED, scoreRun, readQeGrade, scoreReceiptToAggregateRow, readScoreAggregateRows, dedupeScoreAggregateRows, buildScoreAggregateReport, renderScoreAggregateReport, recapWindow, decideHorizon, withinWindow, buildRecap, renderRecap, parseSourceManifest, tgPostHtmlIssues, tgVisibleLength, decideTgSend, TG_TEXT_LIMIT, countRecallEventsForRun, unknownFlagNotice, mirrorWriterExplanation, appendRecallUsage, closenessLine, anyAboveFloor, decideNameCheck, renderNameCheck, exportedNamesIn, dispatchedCommandsIn, decideSourceProvenance, renderSourceProvenance, REFUSED_HORIZONS, renderScorecard, renderCompoundingReport, readReinforcementState, readQuarantineState, registrationExitCode, renderRegistrationReport,
|
|
25
25
|
// Smart Backlog (feature smart-backlog) — goal-directed idea pipeline over the Brain vector engine.
|
|
26
26
|
readBacklogConfig, readIdeas, writeIdeas, ideaId, dedupIdea, readGoalMap, readGoalMapDetailed, parseEffort, ensureBacklogGitignored, isSafeId, alignIdea, mirrorIdeaVector, ensureBacklogEmbedForm, readBacklogEmbedFormVersion, recordAbsorption, DEDUP_EMBED_FORM_VERSION, snapshotIdeas, spinRoulette, rankRoulette, seededRng, eligibleIdeas, stageEnrichment, buildJiraDraft, resolveJiraAdapter, makeBacklogIO, harmonizeBacklog, transitionIdeas, editIdea, clearEmbedStale, BACKLOG_BACKENDS, applyDomainBoost, DZ_OWNED_TASK_TYPES, applyExportHoldout, DEFAULT_HELD_OUT_DOMAINS, canonicalDomainKey, readAgentdbRowsByTaskType, heldOutAfterOptIn, renderHoldoutNote, renderSharedStoreAdvice, decideVectorExport, countDisplacedByCut, renderDomainBoostNote, renderDomainCutNote, parseReqeDebt,
|
|
27
27
|
// qe-bridge (feature qe-bridge-claude, ADR-001): the pure half of the reverse QE bridge.
|
|
28
28
|
KNOWN_CLAUDE, isSafeClaudeId, claudeProbeArgs, claudeReviewArgs, interpretClaudeProbe, modelFamily, buildBridgePrompt, parseBridgeOutput, buildBridgeFailureRecord, buildBridgeSignoffRecord, renderBridgeReport, isSafeSlug, hasUnsafePathChars, hasDotDotSegment, buildReqeBrief, settleReqeDebt, renderReqeList, REQE_SCOPE,
|
|
29
29
|
// Mutation gate (feature ha-mutation-gate) — break each named protection, run the suite, require red.
|
|
30
|
-
parseMutationRegistry, applyMutationToText, countFailingTests, detectSuiteCompletionReceipt, detectSuiteReceiptMismatch, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, TRACE_BUNDLE_LEDGER_PATH, TRACE_BUNDLE_SCHEMA, TRACE_BUNDLE_RUN_META_FILE, buildBundle, serializeBundle, parseBundle, planImport, decideCheckpointWrite, amendmentSection, planSaysNoAmendments, parseAmendments, resolveAmendments, decideAmendmentOutcome, amendmentVerdictLine, amendmentsMissingFromPlan, AMENDMENT_VACUITY_NOTE, extractContractChecklist, parseContractVerdictReport, verifyContractVerdicts, decideSignableSet, signableSetLine, decideRecordWrite, decideReadBack, recordVerdictLine, buildCadenceReport, tgVisibleSha256, CADENCE_WINDOW_DAYS, readQeRounds, QE_ROUNDS_DEFAULT_CEILING, adviseRestart, describeStoreLocation, storeLocationLine, resolveTeachTarget, teachReasonPhrase, readTeachToConfig, TeachTargetError, mergeStoreHits, sameStore, globalStoreRoot, storeCountLabel,
|
|
30
|
+
parseMutationRegistry, applyMutationToText, attributeBaselineRedness, countFailingTests, detectSuiteCompletionReceipt, detectSuiteReceiptMismatch, classifyBaseline, classifyRunFailure, classifyMutationOutcome, mutationGateExitCode, summarizeMutationResults, renderMutationReport, runWithOneInternalRetry, TRACE_BUNDLE_LEDGER_PATH, TRACE_BUNDLE_SCHEMA, TRACE_BUNDLE_RUN_META_FILE, buildBundle, serializeBundle, parseBundle, planImport, decideCheckpointWrite, amendmentSection, planSaysNoAmendments, parseAmendments, resolveAmendments, decideAmendmentOutcome, amendmentVerdictLine, amendmentsMissingFromPlan, AMENDMENT_VACUITY_NOTE, extractContractChecklist, parseContractVerdictReport, verifyContractVerdicts, decideSignableSet, signableSetLine, decideRecordWrite, decideReadBack, recordVerdictLine, buildCadenceReport, tgVisibleSha256, CADENCE_WINDOW_DAYS, readQeRounds, QE_ROUNDS_DEFAULT_CEILING, adviseRestart, describeStoreLocation, storeLocationLine, resolveTeachTarget, teachReasonPhrase, readTeachToConfig, TeachTargetError, mergeStoreHits, sameStore, globalStoreRoot, storeCountLabel,
|
|
31
31
|
// operator-profile (ADR-001): per-user 0600 store + marked block in ~/.claude/CLAUDE.md
|
|
32
32
|
renderProfileBlock, readProfile, writeProfile, syncProfileBlock, checkProfileDrift, parseRegister, registerOwnerWord, profileAgeDays, parseDomainList, domainListText, parseYesNo, REGISTERS, } from '@dzhechkov/harness-core';
|
|
33
33
|
import { getPreset, PRESET_NAMES } from '@dzhechkov/harness-presets';
|
|
@@ -47,7 +47,7 @@ export const DZ_COMMANDS = [
|
|
|
47
47
|
'epoch-replay', 'score', 'recap', 'cadence', 'qe-rounds', 'restart-advisor', 'tg-post',
|
|
48
48
|
'name-check', 'provenance-check', 'feature-adr-record', 'amendment-check', 'contract-check',
|
|
49
49
|
'feature-adr-checkpoint', 'profile', 'reqe', 'qe-bridge', 'backlog', 'routing',
|
|
50
|
-
'bto-optimize', 'dashboard', 'roam', 'import-ecc',
|
|
50
|
+
'bto-optimize', 'dashboard', 'roam', 'import-ecc', 'chain',
|
|
51
51
|
];
|
|
52
52
|
const USAGE = `dz - DZ cross-platform harness CLI
|
|
53
53
|
|
|
@@ -88,6 +88,7 @@ Usage:
|
|
|
88
88
|
dz epoch-replay --judge <filled-work-order.json> [--out <file>] (blind judge prompts from the filled plans)
|
|
89
89
|
dz epoch-replay --score <judgments.json> --work-order <file> [--slice <name>] [--json] (un-blind against the pre-registered assignment → SUPPORTED only when the two 95% Wilson CIs are DISJOINT, else FALSIFIED / INCONCLUSIVE)
|
|
90
90
|
dz score --slug <feature> [--project <dir>] [--json] (process scorecard for ONE feature-adr run, from its artifacts: ADR confirmation, discrimination, cross-model QE grade, live verification, README-first, learning loop, amendments — descriptive-only, a low score exits 0)
|
|
91
|
+
dz score --all [--project <dir>] [--json] (sweep features/*/.fa-state/score-*.json into the append-only chained scorecards aggregate — descriptive-only, always exits 0)
|
|
91
92
|
dz recap [--day|--week|--month] [--at <ISO date>] [--project <dir>] [--json] (what was done over a window, from records only: deliveries with the grade an independent review STATED — a report naming two grades is reported ambiguous, never guessed — registry publishes, gate verdicts, knowledge reuse. --quarter/--half-year/--year are RECOGNISED and REFUSED with the real span in days: there is one complete quarter and the longest record is 174 days. Every section carries its own data-start date, and "the source was not read" never prints as zero. Contaminated measures — commit count, lines, tokens, learning-event volume, inventory counts, lesson count — are not computed, and the report says so. exit 0 reported / 2 refused)
|
|
92
93
|
dz cadence [--window day|week|month|quarter|halfyear|year] [--json] (the WHAT-SHIPPED aggregator: graded-shipment cadence by ISO week + npm-publish cadence (recap cache) + guard repeat decay on the FIXED rule set + recall reuse; a window deeper than 2× the record is REFUSED with the depth named (ADR: a cadence from one point is scale forgery); exit 0 report / 2 refused-window / 1 usage)
|
|
93
94
|
dz qe-rounds (--slug <feature> | --feature-dir <abs>) [--ceiling <n>] [--project <dir>] [--json] (how many Step-8 review rounds has this feature ALREADY had? Reads what dz qe-bridge already wrote — signoff-<runId>.json and failed-*.json under features/<slug>/.fa-state/qe-bridge — and writes nothing itself, so it can answer for runs already past. A round is a runId, not a file; an attempt with no verdict is counted SEPARATELY and never merged; an unreadable record is NAMED and the count is declared a LOWER BOUND. ONE directory, never a union across checkouts. exit 0 under the ceiling / 1 at-or-over — owner decides, the command does not judge whether the rounds were warranted / 2 NOT ESTABLISHED, which is never "zero rounds")
|
|
@@ -106,7 +107,7 @@ Usage:
|
|
|
106
107
|
dz feature-adr-checkpoint (--slug <feature> | --feature-dir <abs>) --stage <s> --input-hash <h> --result <json> [--artifact a,b] [--json] (record a pipeline stage ONLY after measuring its artifacts on disk; refuses a null result, an absent artifact, or a stage that declares none — the subagent runs a COMMAND instead of hand-writing durable state)
|
|
107
108
|
dz profile [init|show|set|sync] [--json] (WHO the assistant is talking to — per-user store at ~/.dz/profile.json (0600, NEVER in a project), delivered as a marked block in ~/.claude/CLAUDE.md so it loads in EVERY project, dz installed or not. init = five questions (language, register, deep/weak domains as comma lists — "networking (CCIE; NSX)" keeps the parenthetical as the note, Enter skips — teaches y/n with one re-ask, never a silent default); show ALWAYS prints the store path + age + drift verdict + the rendered block; set register|language|teaches <v> or set deep|weak add|rm <tag> [note] — register accepts the owner's own words (профи / профи лайт / просто), an unknown value is REFUSED naming the accepted set; sync re-writes the block (runs automatically after init/set; foreign content byte-for-byte, timestamped backup before every modifying write). The register changes FORM, never FACTS, and governs dialogue only — never ADRs/commits/QE reports; both rules are baked into the rendered block at every level. exit 0 done / 1 no profile or failed / 2 refused input)
|
|
108
109
|
dz reqe [--slug <feature> [--done --report <f>]] [--json] (the re-QE debt ledger: a usage-switched run whose Step-8 QE ran on the coder's OWN family records a debt; list debts, print the cross-family review brief, settle FAIL-CLOSED against a graded report — the settlement lands in 08_qe_report.md)
|
|
109
|
-
dz qe-bridge --family claude --slug <feature> [--coder-family codex|claude] [--model <id>] [--files a,b] [--out <f>] [--timeout <s>] [--allow-same-family] [--json] (the REVERSE QE bridge: run an INDEPENDENT Claude reviewer over a feature's Step-8 artifacts from ANY host — a Codex session included, plain shell, no Claude agent plane needed — and land a PARSED signoff. The reviewer runs ISOLATED: an EMPTY temp cwd plus --safe-mode --strict-mcp-config --tools '' --no-session-persistence, so no CLAUDE.md/skills/plugins/hooks/MCP load, and the verdict is read from the --output-format json RESULT ENVELOPE — text a session customization printed onto the same stdout can never become a signoff. Probes the model before trusting it; sends SCOPED extracts with a loud 200k-char ceiling (never silent truncation); the grade must AGREE across three LAST-anchored channels (terminal marker line, fenced qe-bridge-signoff JSON, the report's own GRADE line) AND the marker must be the FINAL content — empty, gradeless, self-contradicting or miscounted output is one of
|
|
110
|
+
dz qe-bridge --family claude --slug <feature> [--coder-family codex|claude] [--model <id>] [--files a,b] [--out <f>] [--timeout <s>] [--allow-same-family] [--json] (the REVERSE QE bridge: run an INDEPENDENT Claude reviewer over a feature's Step-8 artifacts from ANY host — a Codex session included, plain shell, no Claude agent plane needed — and land a PARSED signoff. The reviewer runs ISOLATED: an EMPTY temp cwd plus --safe-mode --strict-mcp-config --tools '' --no-session-persistence, so no CLAUDE.md/skills/plugins/hooks/MCP load, and the verdict is read from the --output-format json RESULT ENVELOPE — text a session customization printed onto the same stdout can never become a signoff. Probes the model before trusting it; sends SCOPED extracts with a loud 200k-char ceiling (never silent truncation); the grade must AGREE across three LAST-anchored channels (terminal marker line, fenced qe-bridge-signoff JSON, the report's own GRADE line) AND the marker must be the FINAL content — empty, gradeless, self-contradicting or miscounted output is one of 17 NAMED failures with an audit record under features/<slug>/.fa-state/qe-bridge/ (runId, resolved executable + binOverride, prompt sha256, channel offsets, requestedOut, reportWritten, retained raw stdout; 0600 files in a 0700 dir), never a clean review. A --coder-family that contradicts the recorded reqe debt is refused. Writes features/<slug>/08b_reqe_report.md, which dz reqe --done settles unchanged. DISCLOSURE: the extracts you scope are sent to the Claude runtime; the bridge cannot classify secrets. DZ_QE_BRIDGE_CLAUDE_BIN is a TEST SEAM, not a flag. exit 0 signoff parsed (ANY grade — it reports, it does not gate) / 1 named failure / 2 usage)
|
|
110
111
|
dz mutation-gate [--package <dir>] [--registry <file>] [--test-cmd "<cmd>"] [--only <id[,id]>] [--timeout <ms>] [--rebaseline per-entry|final] [--keep-scratch] [--json] (prove each NAMED protection has a test that DISCRIMINATES: copy the package to a scratch dir, verify the baseline suite is green, apply each registry mutation, run the suite, REQUIRE red, restore. The red must be BEHAVIOURAL: a mutation that no longer parses is MUTATION_UNPARSEABLE; a red run whose OWN output reports a test FILE failing to load (node --test file-level not-ok with exitCode, vitest Failed Suites) is MUTATION_LOAD_FATAL — the signal comes from the same run as the failing count, never from a separate isolated import; red output whose shape matches no known runner is INCONCLUSIVE (a runner-coverage gap, loud, never PROVEN); a count far above the entry's bound is OVER_FAILING; a restored tree that does not reproduce green makes the entry INCONCLUSIVE (flaky). Mutation writes are realpath-contained to the scratch copy: a symlink escape or a node_modules/ target is refused (exit 2), the real tree is never written. A mutation that does not apply, a green suite, or an inconclusive run is a FAILURE — never a skip. exit 0 all proven / 1 gate failed / 2 setup error)
|
|
111
112
|
dz backlog add "<idea>" [--effort 1-5] [--proposal <text>] [--dry-run] [--project <dir>] [--json] (capture an idea: semantic dedup against existing ideas via the Brain vector engine (DUPLICATE>=0.92 merges, RELATED links, NEW creates) + GoalMap alignment; --dry-run classifies without writing)
|
|
112
113
|
dz backlog list [--status <s>] [--goal <id>] [--project <dir>] [--json] (list captured ideas, filterable by status/goal)
|
|
@@ -145,6 +146,7 @@ Usage:
|
|
|
145
146
|
dz statusline --fa-record --slug <s> --step "<label>" [--kind <feature-adr|loop>] [--recalled <n>] [--stored <n>] [--mode <m>] (feature-adr: record live per-run learning state → 📐 panel segment)
|
|
146
147
|
dz usage [--json] [--project <dir>] | dz usage --calibrate --session <pct> --weekly <pct> [--model fable=<pct>] [--project <dir>] (ESTIMATE Claude usage from fixed reset windows; optional per-model weekly binding; exit 0 ALWAYS; pct=null when limits unconfigured)
|
|
147
148
|
dz usage --by-stage [--run <runId> | --slug <slug>] [--epsilon <0..1>] [--write <file.jsonl>] [--json] (per-stage cost ledger for ONE feature-adr run + the reconciliation invariant: accounted + unaccounted = run total; verdict BALANCED | DEFECT | INSUFFICIENT_DATA; local transcript ESTIMATES — catches ATTRIBUTION errors, not pricing errors)
|
|
149
|
+
dz chain [--project <dir>] [--json] (verify EVERY hash-chained journal in ONE command: coverage is DERIVED from the CHAINED_JOURNALS registry, never typed, so a journal cannot be given a chain and checked by nobody. An ABSENT journal is NAMED absent, never omitted — omission and cleanliness are indistinguishable in a report. Statuses: ok | healed (defects the current unbroken run has outlived — verdicts over present records are sound) | unchained (present, no chained record yet — legal) | absent | broken | unreadable. Exit 1 on broken/unreadable: a verifier that reports damage and exits 0 is one no automation can act on)
|
|
148
150
|
dz claim-check [paths...] [--json] [--fail-on high|medium|none] [--project <dir>] (enforce the Integrity Rule: flag untagged/overstated accuracy claims; default scan = root README.md + every discovered package's README.md + features/*/08_qe_report.md + docs/**/*.md (historical feature artifacts are NOT scanned — pass paths explicitly); exit 1 only at/above --fail-on, default high)
|
|
149
151
|
dz lint [paths...] [--json] [--config <file>] [--registry <file>] [--project <dir>] (advisory EN/RU prose-style lint; findings exit 0, incomplete input/policy exits 1, usage exits 2)
|
|
150
152
|
dz pretrain [--project <dir>]
|
|
@@ -266,6 +268,35 @@ function discoverSkillsDirs(cwd, explicitSkillsDir) {
|
|
|
266
268
|
async function installSkills(opts) {
|
|
267
269
|
const { target, projectRoot, cwd, explicitSkillsDir, select, force, enrich } = opts;
|
|
268
270
|
const skillsDirs = discoverSkillsDirs(cwd, explicitSkillsDir);
|
|
271
|
+
// PREFLIGHT (backlog 9d15b9b6, PR-A) — resolve the REQUEST once, before anything is written.
|
|
272
|
+
//
|
|
273
|
+
// Two defects lived in asking each root independently instead of resolving the request: a skill
|
|
274
|
+
// present in two roots was installed TWICE and counted twice (the field report's `2 skill(s)` was
|
|
275
|
+
// one skill installed twice), and a skill present in NO root produced a warning and exit 0 —
|
|
276
|
+
// `0 skill(s)` reading as success. Both are gone once the decision happens here.
|
|
277
|
+
//
|
|
278
|
+
// Placement is load-bearing: an exit 1 that arrives after hooks and memory are written leaves a
|
|
279
|
+
// half-configured project, which is worse than either clean outcome. This runs before the loop
|
|
280
|
+
// below and before every target adapter.
|
|
281
|
+
//
|
|
282
|
+
// Dependency closure is deliberately NOT resolved here — that is PR-B. This preflight fixes the
|
|
283
|
+
// count and the exit contract, and gives that work a base it can trust.
|
|
284
|
+
if (select !== undefined) {
|
|
285
|
+
const roots = skillsDirs.map((dir) => ({ dir, ids: discoverSkillIds(dir) }));
|
|
286
|
+
const resolution = resolveSelection(select, roots);
|
|
287
|
+
for (const shadow of resolution.shadowed) {
|
|
288
|
+
opts.writeErr?.(`dz: skill '${shadow.id}' is offered by ${shadow.alsoIn.length + 1} roots; ` +
|
|
289
|
+
`installing from ${shadow.chosen} (earlier root wins). Also present in: ${shadow.alsoIn.join(', ')}`);
|
|
290
|
+
}
|
|
291
|
+
const refusal = formatSelectRefusal(resolution, roots);
|
|
292
|
+
if (refusal !== null) {
|
|
293
|
+
return {
|
|
294
|
+
selectRefusal: refusal,
|
|
295
|
+
results: [], dirsSearched: skillsDirs.length, written: 0, skipped: 0,
|
|
296
|
+
missing: [...resolution.missing], failures: [], applyFailures: [], integrations: [],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
269
300
|
// agents-md and gemini are FLATTENING single-file targets: each must aggregate
|
|
270
301
|
// every selected skill from ALL discovered dirs into ONE root file (AGENTS.md /
|
|
271
302
|
// GEMINI.md) in a single merge. A per-dir runInit loop (like the tree targets
|
|
@@ -398,6 +429,7 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
|
|
|
398
429
|
cwd,
|
|
399
430
|
explicitSkillsDir,
|
|
400
431
|
select,
|
|
432
|
+
writeErr,
|
|
401
433
|
force: flags.has('force'),
|
|
402
434
|
enrich: flags.has('enrich'),
|
|
403
435
|
noHooks: flags.has('no-hooks'),
|
|
@@ -405,6 +437,12 @@ async function cmdInit(options, flags, cwd, write, writeErr) {
|
|
|
405
437
|
noVerify: flags.has('no-verify'),
|
|
406
438
|
...(options.get('allow-integrations') !== undefined ? { allowIntegrations: options.get('allow-integrations') } : {}),
|
|
407
439
|
});
|
|
440
|
+
// PR-A: an explicit --select that named a skill no root provides is a REFUSAL, not a warning.
|
|
441
|
+
// Printed and returned here, before any target adapter runs — nothing has been written yet.
|
|
442
|
+
if (r.selectRefusal !== undefined) {
|
|
443
|
+
writeErr(r.selectRefusal);
|
|
444
|
+
return 1;
|
|
445
|
+
}
|
|
408
446
|
// Codex keeps its established user-registry writer, but its result is normalized into the same
|
|
409
447
|
// two-outcome contract before JSON/human rendering. A write without a live ready observation is
|
|
410
448
|
// a refusal with applied=true, never a second success channel.
|
|
@@ -4860,6 +4898,28 @@ async function cmdSetup(options, flags, cwd, write, writeErr) {
|
|
|
4860
4898
|
writeErr(formatTargetAliasNote('dz setup', targetOpt, target));
|
|
4861
4899
|
const projectRoot = resolve(cwd, options.get('project') ?? '.');
|
|
4862
4900
|
const presetName = options.get('preset');
|
|
4901
|
+
// PREFLIGHT BEFORE THE FIRST WRITE (backlog 9d15b9b6, PR-A). Step 3 configures the learning
|
|
4902
|
+
// environment and step 4 installs skills, so refusing at step 4 would leave a project that has
|
|
4903
|
+
// memory and hooks but not the skills the operator asked for — a half-configured state worse than
|
|
4904
|
+
// either clean outcome. The request is therefore resolved HERE, before the banner's first step.
|
|
4905
|
+
//
|
|
4906
|
+
// Only an EXPLICIT --select is refused. A preset names skills the package itself ships, so a gap
|
|
4907
|
+
// there is our packaging defect, not the operator's typo, and it is reported by the existing
|
|
4908
|
+
// missing-list rather than by refusing the whole run.
|
|
4909
|
+
const setupSelectRaw = options.get('select');
|
|
4910
|
+
if (setupSelectRaw !== undefined) {
|
|
4911
|
+
const requested = setupSelectRaw.split(',').map((x) => x.trim()).filter((x) => x.length > 0);
|
|
4912
|
+
const roots = discoverSkillsDirs(cwd, options.get('skills-dir')).map((dir) => ({ dir, ids: discoverSkillIds(dir) }));
|
|
4913
|
+
const resolution = resolveSelection(requested, roots);
|
|
4914
|
+
for (const shadow of resolution.shadowed) {
|
|
4915
|
+
writeErr(`dz: skill '${shadow.id}' is offered by ${shadow.alsoIn.length + 1} roots; installing from ${shadow.chosen} (earlier root wins). Also present in: ${shadow.alsoIn.join(', ')}`);
|
|
4916
|
+
}
|
|
4917
|
+
const refusal = formatSelectRefusal(resolution, roots);
|
|
4918
|
+
if (refusal !== null) {
|
|
4919
|
+
writeErr(refusal);
|
|
4920
|
+
return 1;
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4863
4923
|
write(`\n╔══════════════════════════════════════════════════════╗`);
|
|
4864
4924
|
write(`║ DZ SETUP — Full Environment ║`);
|
|
4865
4925
|
write(`╠══════════════════════════════════════════════════════╣`);
|
|
@@ -5001,7 +5061,7 @@ function cmdPretrain(options, cwd, write) {
|
|
|
5001
5061
|
write(`╚══════════════════════════════════════════════════════════════╝`);
|
|
5002
5062
|
return 0;
|
|
5003
5063
|
}
|
|
5004
|
-
function cmdRecommend(options, cwd, write) {
|
|
5064
|
+
function cmdRecommend(options, flags, cwd, write) {
|
|
5005
5065
|
const task = options.get('_positional_0');
|
|
5006
5066
|
if (!task) {
|
|
5007
5067
|
write('dz recommend: task description required');
|
|
@@ -5010,15 +5070,40 @@ function cmdRecommend(options, cwd, write) {
|
|
|
5010
5070
|
}
|
|
5011
5071
|
const registry = buildRegistry(cwd);
|
|
5012
5072
|
const report = recommend(task, registry, cwd);
|
|
5073
|
+
if (flags.has('json')) {
|
|
5074
|
+
write(JSON.stringify(report, null, 2));
|
|
5075
|
+
return 0;
|
|
5076
|
+
}
|
|
5013
5077
|
write(`\n╔══════════════════════════════════════════════════════════════╗`);
|
|
5014
5078
|
write(`║ DZ RECOMMEND — Task Advisor ║`);
|
|
5015
5079
|
write(`╠══════════════════════════════════════════════════════════════╣`);
|
|
5016
5080
|
write(`║ Task: ${report.task.slice(0, 52).padEnd(52)}║`);
|
|
5017
|
-
|
|
5018
|
-
|
|
5081
|
+
if (report.topicSource === 'task') {
|
|
5082
|
+
write(`║ Topics: ${report.topics.join(', ').slice(0, 50).padEnd(50)}║`);
|
|
5083
|
+
}
|
|
5084
|
+
else if (report.topicSource === 'project-stack') {
|
|
5085
|
+
write(`║ Topics: ${'not matched in the question'.padEnd(50)}║`);
|
|
5086
|
+
}
|
|
5087
|
+
else {
|
|
5088
|
+
write(`║ Topics: ${'not recognized — no recommendations'.padEnd(50)}║`);
|
|
5089
|
+
}
|
|
5019
5090
|
write(`╠══════════════════════════════════════════════════════════════╣`);
|
|
5091
|
+
if (report.topicSource === 'project-stack') {
|
|
5092
|
+
write(`⚠ Тема запроса не распознана — подбор ниже сделан по СТЕКУ ПРОЕКТА, не по вашему вопросу.`);
|
|
5093
|
+
write(` (topic not recognized — recommendations reflect the project stack, not the question)`);
|
|
5094
|
+
write(`PROJECT-STACK SUGGESTIONS`);
|
|
5095
|
+
}
|
|
5096
|
+
else if (report.topicSource === 'none') {
|
|
5097
|
+
write(`Тема запроса не распознана; рекомендаций нет.`);
|
|
5098
|
+
write(`Переформулируйте задачу или используйте dz registry search <слово> / /skill-advisor.`);
|
|
5099
|
+
write(`╚══════════════════════════════════════════════════════════════╝`);
|
|
5100
|
+
return 0;
|
|
5101
|
+
}
|
|
5102
|
+
const stackDerived = report.topicSource === 'project-stack';
|
|
5020
5103
|
if (report.presets.length > 0) {
|
|
5021
|
-
write(
|
|
5104
|
+
write(stackDerived
|
|
5105
|
+
? `║ PROJECT-STACK PRESETS ║`
|
|
5106
|
+
: `║ RECOMMENDED PRESETS ║`);
|
|
5022
5107
|
for (const p of report.presets) {
|
|
5023
5108
|
const matched = p.matchedSkills.length > 0 ? ` (${p.matchedSkills.slice(0, 3).join(', ')})` : '';
|
|
5024
5109
|
write(`║ ${p.name.padEnd(15)} ${String(p.skills).padStart(2)} skills coverage: ${String(p.coverage).padStart(2)} topics${matched.padEnd(15)}║`);
|
|
@@ -5026,7 +5111,9 @@ function cmdRecommend(options, cwd, write) {
|
|
|
5026
5111
|
write(`║${''.padEnd(62)}║`);
|
|
5027
5112
|
}
|
|
5028
5113
|
if (report.skills.length > 0) {
|
|
5029
|
-
write(
|
|
5114
|
+
write(stackDerived
|
|
5115
|
+
? `║ PROJECT-STACK SKILLS (top ${Math.min(report.skills.length, 8)})${' '.repeat(35)}║`
|
|
5116
|
+
: `║ RECOMMENDED SKILLS (top ${Math.min(report.skills.length, 8)})${' '.repeat(35)}║`);
|
|
5030
5117
|
for (const s of report.skills.slice(0, 8)) {
|
|
5031
5118
|
const desc = s.description.length > 35 ? s.description.slice(0, 32) + '...' : s.description;
|
|
5032
5119
|
write(`║ ${s.id.padEnd(24)} ${desc.padEnd(36)}║`);
|
|
@@ -5034,7 +5121,9 @@ function cmdRecommend(options, cwd, write) {
|
|
|
5034
5121
|
write(`║${''.padEnd(62)}║`);
|
|
5035
5122
|
}
|
|
5036
5123
|
if (report.toolkits.length > 0) {
|
|
5037
|
-
write(
|
|
5124
|
+
write(stackDerived
|
|
5125
|
+
? `║ PROJECT-STACK PIPELINE (npx toolkits) ║`
|
|
5126
|
+
: `║ FULL PIPELINE (npx toolkits) ║`);
|
|
5038
5127
|
for (const tk of report.toolkits) {
|
|
5039
5128
|
const desc = tk.description.length > 44 ? tk.description.slice(0, 41) + '...' : tk.description;
|
|
5040
5129
|
write(`║ ${tk.name.padEnd(16)} ${desc.padEnd(44)}║`);
|
|
@@ -5044,13 +5133,17 @@ function cmdRecommend(options, cwd, write) {
|
|
|
5044
5133
|
write(`║${''.padEnd(62)}║`);
|
|
5045
5134
|
}
|
|
5046
5135
|
write(`╠══════════════════════════════════════════════════════════════╣`);
|
|
5047
|
-
write(
|
|
5136
|
+
write(stackDerived
|
|
5137
|
+
? `║ PROJECT-STACK PLAN ║`
|
|
5138
|
+
: `║ STEP-BY-STEP PLAN ║`);
|
|
5048
5139
|
for (const step of report.plan) {
|
|
5049
5140
|
const line = step.length > 60 ? step.slice(0, 57) + '...' : step;
|
|
5050
5141
|
write(`║ ${line.padEnd(58)}║`);
|
|
5051
5142
|
}
|
|
5052
5143
|
write(`╠══════════════════════════════════════════════════════════════╣`);
|
|
5053
|
-
write(
|
|
5144
|
+
write(stackDerived
|
|
5145
|
+
? `║ PROJECT-STACK QUICK INSTALL ║`
|
|
5146
|
+
: `║ QUICK INSTALL ║`);
|
|
5054
5147
|
const cmd = report.installCommand.length > 58 ? report.installCommand.slice(0, 55) + '...' : report.installCommand;
|
|
5055
5148
|
write(`║ ${cmd.padEnd(58)}║`);
|
|
5056
5149
|
write(`╚══════════════════════════════════════════════════════════════╝`);
|
|
@@ -6811,6 +6904,81 @@ function looksBinaryText(text) {
|
|
|
6811
6904
|
* per-file findings (each enriched with its `file`), and applies the exit-code contract.
|
|
6812
6905
|
* `--json` ALWAYS emits valid JSON `{ok, findings, scanned}`, even on the failure path.
|
|
6813
6906
|
*/
|
|
6907
|
+
/**
|
|
6908
|
+
* `dz chain` — verify EVERY hash-chained journal in one command (W0-chain, backlog bc4ee35c).
|
|
6909
|
+
*
|
|
6910
|
+
* The machinery to verify a chain has worked for weeks. What was missing is the ABILITY TO ASK:
|
|
6911
|
+
* verification lived inside two consumers, each carrying its own hardcoded list of which files are
|
|
6912
|
+
* chained, so a journal could be given a chain and still be checked by nobody. Coverage here is
|
|
6913
|
+
* DERIVED from CHAINED_JOURNALS, never typed — adding a journal to the registry adds it to this
|
|
6914
|
+
* report by construction.
|
|
6915
|
+
*
|
|
6916
|
+
* An ABSENT journal is reported as `absent`, not omitted. Omission and cleanliness are
|
|
6917
|
+
* indistinguishable in a report, and that indistinguishability is how the original blind spot
|
|
6918
|
+
* survived; the same reason `broken` exits NON-ZERO rather than merely printing — a verifier that
|
|
6919
|
+
* reports damage and exits 0 is one no automation can act on, and this verb exists to run unattended.
|
|
6920
|
+
*
|
|
6921
|
+
* A journal that exists but carries NO chained records is `unchained`, which is legal (a log may
|
|
6922
|
+
* predate the chain) and therefore does not fail the command. Calling it a defect would train the
|
|
6923
|
+
* reader to ignore the output — the failure mode already measured once on the doctor's own line.
|
|
6924
|
+
*/
|
|
6925
|
+
function cmdChain(options, flags, cwd, write) {
|
|
6926
|
+
const root = options.get('project') ?? cwd;
|
|
6927
|
+
const journals = CHAINED_JOURNALS.map((journal) => {
|
|
6928
|
+
const path = join(root, journal.rel);
|
|
6929
|
+
if (!existsSync(path)) {
|
|
6930
|
+
return { rel: journal.rel, decides: journal.decides, status: 'absent', chained: 0, defects: 0, detail: 'file not present' };
|
|
6931
|
+
}
|
|
6932
|
+
let text = '';
|
|
6933
|
+
try {
|
|
6934
|
+
text = readFileSync(path, 'utf-8');
|
|
6935
|
+
}
|
|
6936
|
+
catch {
|
|
6937
|
+
// Unreadable is NOT clean. It is the one outcome that must never be quietly folded into
|
|
6938
|
+
// "nothing to report": we did not look, so we know nothing.
|
|
6939
|
+
return { rel: journal.rel, decides: journal.decides, status: 'unreadable', chained: 0, defects: 0, detail: 'file could not be read' };
|
|
6940
|
+
}
|
|
6941
|
+
const v = verifyEventChainText(text);
|
|
6942
|
+
if (v.chained === 0) {
|
|
6943
|
+
return { rel: journal.rel, decides: journal.decides, status: 'unchained', chained: 0, defects: 0, detail: 'present, but no record carries a chain (legal — the log predates chaining)' };
|
|
6944
|
+
}
|
|
6945
|
+
const total = text.split('\n').filter((l) => l.trim() !== '').length;
|
|
6946
|
+
const age = classifyChainDefects(v, total);
|
|
6947
|
+
if (v.ok) {
|
|
6948
|
+
return { rel: journal.rel, decides: journal.decides, status: 'ok', chained: v.chained, defects: 0, detail: `${v.chained} chained record(s), ${v.resets} recorded restart(s)` };
|
|
6949
|
+
}
|
|
6950
|
+
// A break the current unbroken run has already outlived does not make TODAY's records unsound.
|
|
6951
|
+
// Reporting both alike is what made the doctor's equivalent line permanently red for four weeks.
|
|
6952
|
+
const historical = age.inRun.length === 0 && age.runRecords > 0;
|
|
6953
|
+
return {
|
|
6954
|
+
rel: journal.rel,
|
|
6955
|
+
decides: journal.decides,
|
|
6956
|
+
status: historical ? 'healed' : 'broken',
|
|
6957
|
+
chained: v.chained,
|
|
6958
|
+
defects: v.defects.length,
|
|
6959
|
+
detail: historical
|
|
6960
|
+
? `${v.defects.length} defect(s), all BEFORE the current run — the last ${age.runRecords} record(s) are unbroken, so verdicts over those are sound`
|
|
6961
|
+
: `${v.defects.length} defect(s) with NO sound records after them: verdicts computed from this log are unsafe`,
|
|
6962
|
+
};
|
|
6963
|
+
});
|
|
6964
|
+
const failed = journals.filter((j) => j.status === 'broken' || j.status === 'unreadable');
|
|
6965
|
+
const ok = failed.length === 0;
|
|
6966
|
+
if (flags.has('json')) {
|
|
6967
|
+
write(JSON.stringify({ ok, root, journals }, null, 2));
|
|
6968
|
+
return ok ? 0 : 1;
|
|
6969
|
+
}
|
|
6970
|
+
write(`dz chain — ${journals.length} registered journal(s) under ${root}`);
|
|
6971
|
+
write('');
|
|
6972
|
+
const MARK = { ok: '\u2713', healed: '\u2713', unchained: '\u00b7', absent: '\u00b7', broken: '\u2717', unreadable: '\u2717' };
|
|
6973
|
+
for (const j of journals) {
|
|
6974
|
+
write(` ${MARK[j.status] ?? '?'} ${j.rel} — ${j.status}`);
|
|
6975
|
+
write(` ${j.detail}`);
|
|
6976
|
+
write(` decides: ${j.decides}`);
|
|
6977
|
+
}
|
|
6978
|
+
write('');
|
|
6979
|
+
write(ok ? ' all registered journals are sound for present verdicts' : ` ${failed.length} journal(s) UNSAFE — see above`);
|
|
6980
|
+
return ok ? 0 : 1;
|
|
6981
|
+
}
|
|
6814
6982
|
function cmdClaimCheck(options, _optionLists, flags, cwd, write) {
|
|
6815
6983
|
// Reject (never silently coerce) an invalid --fail-on.
|
|
6816
6984
|
const failOnRaw = options.get('fail-on') ?? 'high';
|
|
@@ -9611,8 +9779,79 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
|
|
|
9611
9779
|
// R11: a hung runner is a loud non-answer, never a pass. Same default + parse shape as mutation-gate.
|
|
9612
9780
|
const timeoutOpt = Number(options.get('timeout') ?? '300000');
|
|
9613
9781
|
const timeoutMs = Number.isFinite(timeoutOpt) && timeoutOpt > 0 ? timeoutOpt : 300000;
|
|
9614
|
-
|
|
9782
|
+
// Runner honesty (feature instrument-honesty, ADR-001): the runner is selected from the TARGET
|
|
9783
|
+
// package's own scripts.test, never from a global default. The package dir is the nearest
|
|
9784
|
+
// ancestor of the FIRST named test that carries a package.json — walked here, at the seam,
|
|
9785
|
+
// because the pure half deliberately takes the script text as data and never touches the fs.
|
|
9786
|
+
let packageTestScript = null;
|
|
9787
|
+
let packageDevDependencies = [];
|
|
9788
|
+
let packageDir = repoRoot;
|
|
9789
|
+
{
|
|
9790
|
+
const firstTest = propertyTests[0]?.file;
|
|
9791
|
+
// QE-1 (instrument-honesty, HIGH): this walk runs on the RAW --test argument, BEFORE the
|
|
9792
|
+
// engine's sanitation — a `../` traversal made it read an arbitrary package.json OUTSIDE the
|
|
9793
|
+
// repo and echo its scripts.test verbatim into the JSON output (MEASURED with a planted
|
|
9794
|
+
// marker file). Containment first: a start point outside the repo root never gets walked,
|
|
9795
|
+
// the script stays null, and the engine's own path sanitation then refuses the test path.
|
|
9796
|
+
const walkStart = firstTest !== undefined ? resolve(cwd, dirname(firstTest)) : undefined;
|
|
9797
|
+
if (firstTest !== undefined && walkStart !== undefined
|
|
9798
|
+
&& (walkStart === resolve(repoRoot) || walkStart.startsWith(resolve(repoRoot) + sep))) {
|
|
9799
|
+
let probe = walkStart;
|
|
9800
|
+
// walk up to the repo root looking for package.json (bounded by the fs root either way)
|
|
9801
|
+
for (;;) {
|
|
9802
|
+
if (existsSync(join(probe, 'package.json'))) {
|
|
9803
|
+
packageDir = probe;
|
|
9804
|
+
break;
|
|
9805
|
+
}
|
|
9806
|
+
const parent = dirname(probe);
|
|
9807
|
+
if (parent === probe || probe === repoRoot)
|
|
9808
|
+
break;
|
|
9809
|
+
probe = parent;
|
|
9810
|
+
}
|
|
9811
|
+
try {
|
|
9812
|
+
const pkg = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf-8'));
|
|
9813
|
+
packageTestScript = typeof pkg.scripts?.test === 'string' ? pkg.scripts.test : null;
|
|
9814
|
+
packageDevDependencies = Object.keys(pkg.devDependencies ?? {});
|
|
9815
|
+
}
|
|
9816
|
+
catch { /* unreadable package.json → selection falls through to the honest REFUSE */ }
|
|
9817
|
+
}
|
|
9818
|
+
}
|
|
9819
|
+
// The pure half's path sanitation expects a REPO-RELATIVE package dir ('.'-rooted), not an
|
|
9820
|
+
// absolute one — an absolute path is refused as unsafe-package-dir by design.
|
|
9821
|
+
const packageDirRel = relative(repoRoot, packageDir) || '.';
|
|
9822
|
+
const planInput = runnerOpt !== undefined
|
|
9823
|
+
? { baseRef, propertyTests, runner: runnerOpt, packageTestScript, packageDevDependencies, packageDir: packageDirRel }
|
|
9824
|
+
: { baseRef, propertyTests, packageTestScript, packageDevDependencies, packageDir: packageDirRel };
|
|
9825
|
+
const plan = planDiscriminationCheck(planInput);
|
|
9615
9826
|
if (!plan.runnable) {
|
|
9827
|
+
// QE-2 (instrument-honesty, MEDIUM): a runner REFUSE used to be reported through the generic
|
|
9828
|
+
// "no property test to check"/map-a-test framing — the operator-facing surface re-created the
|
|
9829
|
+
// exact "instrument gap misread as test gap" class ADR-001 names as the reason three duplicate
|
|
9830
|
+
// backlog entries existed. The plan's own named reason is the verdict; the generic classify
|
|
9831
|
+
// stays only for the genuinely-empty-target case.
|
|
9832
|
+
const runnerRefusal = typeof plan.reason === 'string' && plan.reason.startsWith('unsupported-runner');
|
|
9833
|
+
if (runnerRefusal) {
|
|
9834
|
+
const refusal = {
|
|
9835
|
+
aggregate: 'CANNOT_ISOLATE',
|
|
9836
|
+
measurementValid: false,
|
|
9837
|
+
primaryAction: plan.primaryAction ?? 'fix-runner-invocation',
|
|
9838
|
+
finding: {
|
|
9839
|
+
severity: 'high',
|
|
9840
|
+
verdict: 'CANNOT_ISOLATE',
|
|
9841
|
+
files: plan.targets.map((t) => t.file),
|
|
9842
|
+
detail: `runner refused: ${plan.reason} — the INSTRUMENT could not run, nothing was measured; `
|
|
9843
|
+
+ `declare scripts.test in the target package (or pass --runner) and re-run. `
|
|
9844
|
+
+ `This is NOT a statement about the tests.`,
|
|
9845
|
+
},
|
|
9846
|
+
};
|
|
9847
|
+
if (flags.has('json')) {
|
|
9848
|
+
write(JSON.stringify({ plan, results: [], perTest: [], ...refusal }, null, 2));
|
|
9849
|
+
return 0;
|
|
9850
|
+
}
|
|
9851
|
+
write(`discrimination-check: REFUSED (${plan.reason})`);
|
|
9852
|
+
write(` → ${refusal.finding.detail}`);
|
|
9853
|
+
return 0;
|
|
9854
|
+
}
|
|
9616
9855
|
// No safe target to run → this is the existing "property untested" finding (empty propertyTests classify).
|
|
9617
9856
|
const result = classifyDiscrimination({ propertyTests: [], results: [] });
|
|
9618
9857
|
if (flags.has('json')) {
|
|
@@ -9717,10 +9956,24 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
|
|
|
9717
9956
|
}
|
|
9718
9957
|
// t.file + t.name already passed the engine's strict sanitation (no quotes/metacharacters/leading-dash);
|
|
9719
9958
|
// still quote + `--` so a path can never be read as a runner option or split a word.
|
|
9959
|
+
// Runner honesty (ADR-001): the run executes FROM the target package dir with a
|
|
9960
|
+
// package-relative path — a root-cwd `npx vitest run packages/...` loads the ROOT config
|
|
9961
|
+
// (none) and reds unclassifiably, which is exactly the CANNOT_ISOLATE artifact this
|
|
9962
|
+
// feature removes. The plan's own commands encode the same cd; this body mirrors it.
|
|
9963
|
+
const pkgRel = plan.packageDir === '.' ? '' : plan.packageDir;
|
|
9964
|
+
const fileInPkg = pkgRel !== '' && t.file.startsWith(pkgRel + '/') ? t.file.slice(pkgRel.length + 1) : t.file;
|
|
9965
|
+
const execDirBase = pkgRel === '' ? worktree : join(worktree, pkgRel);
|
|
9966
|
+
const execDirTip = pkgRel === '' ? repoRoot : join(repoRoot, pkgRel);
|
|
9720
9967
|
const nameArg = t.name ? ` -t '${t.name}'` : '';
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9968
|
+
// NO `--` before the path: MEASURED 2026-09-02 — `npx vitest run -- 'file'` IGNORES the
|
|
9969
|
+
// filter and runs the whole suite (5269 tests), which is the exact whole-repo artifact
|
|
9970
|
+
// this feature removes (QE ha-intake-archive F5). The path is engine-sanitized (no
|
|
9971
|
+
// leading dash, no metacharacters), so it can never be read as an option.
|
|
9972
|
+
const cmd = `${runner}${nameArg} '${fileInPkg}'`;
|
|
9973
|
+
const base = runCapturedTest(cmd, execDirBase, timeoutMs);
|
|
9974
|
+
// The classifier's targetSeen is a substring probe: the run now prints PACKAGE-relative
|
|
9975
|
+
// paths, so it must be probed with the same form, or every hit reads as target-unseen.
|
|
9976
|
+
const evidence = classifyExecutionEvidence(base.output, base.exitCode, fileInPkg);
|
|
9724
9977
|
const outcome = discriminationOutcomeOf(base.exitCode, evidence);
|
|
9725
9978
|
const row = t.name !== undefined
|
|
9726
9979
|
? { file: t.file, name: t.name, outcome, evidence }
|
|
@@ -9731,8 +9984,8 @@ function cmdDiscriminationCheck(options, flags, cwd, write) {
|
|
|
9731
9984
|
// base rows per the matrix; running it is cheap and only ever on an already-broken path.
|
|
9732
9985
|
// Do NOT "simplify" this to evidenced-error-only — that silently breaks Confirmation 17.
|
|
9733
9986
|
if (base.exitCode !== null && base.exitCode !== 0 && evidence.failureKind !== 'assertions') {
|
|
9734
|
-
const tip = runCapturedTest(cmd,
|
|
9735
|
-
const tipEvidence = classifyExecutionEvidence(tip.output, tip.exitCode,
|
|
9987
|
+
const tip = runCapturedTest(cmd, execDirTip, timeoutMs);
|
|
9988
|
+
const tipEvidence = classifyExecutionEvidence(tip.output, tip.exitCode, fileInPkg);
|
|
9736
9989
|
row['tipOutcome'] = discriminationOutcomeOf(tip.exitCode, tipEvidence);
|
|
9737
9990
|
row['tipEvidence'] = tipEvidence;
|
|
9738
9991
|
// R15, named honestly: the base run is isolated in a worktree, but the tip runs in the LIVE
|
|
@@ -9841,51 +10094,6 @@ function readTipTreeConditions(repoRoot) {
|
|
|
9841
10094
|
function nameFor(t, outcome) {
|
|
9842
10095
|
return t.name !== undefined ? { file: t.file, name: t.name, outcome } : { file: t.file, outcome };
|
|
9843
10096
|
}
|
|
9844
|
-
/**
|
|
9845
|
-
* `dz mutation-gate` — the mutation gate (feature ha-mutation-gate, SPEC at
|
|
9846
|
-
* features/ha-mutation-gate/SPEC.md). A green test proves the code works; it does NOT prove the
|
|
9847
|
-
* test would notice the protection being DELETED. For each entry in a declarative registry this
|
|
9848
|
-
* gate applies the entry's exact {find, replace} mutation to a SCRATCH COPY of the package, runs
|
|
9849
|
-
* the package's test command there, and REQUIRES a non-zero exit (red). All logic is in
|
|
9850
|
-
* harness-core's pure engine (mutation-gate.ts); this is the I/O executor.
|
|
9851
|
-
*
|
|
9852
|
-
* --package <dir> the package to gate (default: cwd; must contain package.json)
|
|
9853
|
-
* --registry <file> registry path (default: <pkg>/test/mutation-registry.json, then
|
|
9854
|
-
* <pkg>/mutation-registry.json)
|
|
9855
|
-
* --test-cmd '<cmd>' suite command run in the scratch copy (default: registry.testCommand,
|
|
9856
|
-
* then `npm test`)
|
|
9857
|
-
* --only <id[,id]> run a subset of entries (unknown id = usage error, never a silent skip)
|
|
9858
|
-
* --timeout <ms> per-suite-run timeout (default 300000). A timed-out run is INCONCLUSIVE —
|
|
9859
|
-
* a FAILURE, never a pass.
|
|
9860
|
-
* --rebaseline <m> route-b guard mode: 'per-entry' (default — every red entry re-runs the
|
|
9861
|
-
* suite on the restored tree; not green ⇒ that entry is INCONCLUSIVE) or
|
|
9862
|
-
* 'final' (one re-run at the end; not green ⇒ every red-based verdict is
|
|
9863
|
-
* downgraded). The gate's PROVEN now means the redness was ATTRIBUTABLE.
|
|
9864
|
-
* --keep-scratch keep the scratch copy for inspection (default: removed in a finally)
|
|
9865
|
-
* --json machine contract {packageDir, registryPath, testCommand, rebaselineMode,
|
|
9866
|
-
* baseline, results, summary, warnings, exitCode}
|
|
9867
|
-
*
|
|
9868
|
-
* The four rules (SPEC §"Four rules") and where each is enforced:
|
|
9869
|
-
* 1. does-not-apply = FAILURE → core classifyMutationOutcome (occurrences !== 1 ⇒ NOT_APPLIED);
|
|
9870
|
-
* 2. green suite = FAILURE → core (exit 0 ⇒ UNDEFENDED, names the property);
|
|
9871
|
-
* 3. never mutate the working tree → HERE: every write targets the scratch copy under tmpdir();
|
|
9872
|
-
* the repo tree is opened read-only, and a crashed run leaves at worst a stale tmp dir;
|
|
9873
|
-
* 4. the gate's own discrimination proof → harness-cli/test/fixtures/mutation-gate-undefended
|
|
9874
|
-
* (the gate MUST fail on it; asserted by test/mutation-gate-cli.test.ts).
|
|
9875
|
-
*
|
|
9876
|
-
* Exit codes: 0 every entry PROVEN · 1 the gate ran and failed (undefended / not-applied /
|
|
9877
|
-
* below-min / unparseable / load-fatal / over-failing / inconclusive entry) · 2 usage or setup
|
|
9878
|
-
* error (missing registry, red BASELINE — a red unmutated copy proves nothing and must not be
|
|
9879
|
-
* read as a mutation result — or an entry whose file RESOLVES outside the scratch copy: a
|
|
9880
|
-
* symlink escape is refused before anything is written, SPEC rule 3).
|
|
9881
|
-
*/
|
|
9882
|
-
/**
|
|
9883
|
-
* Route-a guard for `dz mutation-gate`: parse-check a MUTATED file as its own language BEFORE the
|
|
9884
|
-
* suite runs. A registry mutation must delete the protection while keeping the file loadable — a
|
|
9885
|
-
* file that no longer parses kills the whole suite (or its import chain), and that STRUCTURAL
|
|
9886
|
-
* redness says nothing about the named protection. Returns `{error}` when a parser ran and the
|
|
9887
|
-
* text does not parse; `{skipped}` (reported loudly, never silently) when no parser is available.
|
|
9888
|
-
*/
|
|
9889
10097
|
function parseCheckMutatedFile(absFile, text) {
|
|
9890
10098
|
const ext = extname(absFile).toLowerCase();
|
|
9891
10099
|
try {
|
|
@@ -9916,18 +10124,36 @@ function parseCheckMutatedFile(absFile, text) {
|
|
|
9916
10124
|
}
|
|
9917
10125
|
}
|
|
9918
10126
|
if (ext === '.js' || ext === '.cjs' || ext === '.mjs' || ext === '') {
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
10127
|
+
const checked = runWithOneInternalRetry(() => {
|
|
10128
|
+
try {
|
|
10129
|
+
// `node --check` on the file IN PLACE, so the nearest package.json decides the module goal.
|
|
10130
|
+
execFileSync(process.execPath, ['--check', absFile], { stdio: 'pipe' });
|
|
10131
|
+
return {};
|
|
10132
|
+
}
|
|
10133
|
+
catch (e) {
|
|
10134
|
+
const err = e;
|
|
10135
|
+
// A launched parser that exits non-zero with a SyntaxError is a parse verdict. A child
|
|
10136
|
+
// launch/internal error (EPERM, ENOENT, Node's thrown internal) is runner infrastructure
|
|
10137
|
+
// and must take the bounded retry → INCONCLUSIVE route instead of masquerading as bad JS.
|
|
10138
|
+
if (typeof err.code === 'string' || typeof err.status !== 'number')
|
|
10139
|
+
throw e;
|
|
10140
|
+
const stderrLines = String(err.stderr ?? '').split('\n').map((line) => line.trim()).filter((line) => line !== '');
|
|
10141
|
+
const msg = [...stderrLines].reverse().find((line) => line.includes('Error'))
|
|
10142
|
+
?? stderrLines.at(-1)
|
|
10143
|
+
?? err.message
|
|
10144
|
+
?? 'node --check failed';
|
|
10145
|
+
return { error: msg.slice(0, 200) };
|
|
10146
|
+
}
|
|
10147
|
+
});
|
|
10148
|
+
if (checked.value === null) {
|
|
10149
|
+
return {
|
|
10150
|
+
internalFailureReason: checked.failureReason ?? 'runner-internal-error: persistent after 2/2 attempts',
|
|
10151
|
+
internalAttempts: checked.attempts,
|
|
10152
|
+
};
|
|
9930
10153
|
}
|
|
10154
|
+
return checked.internalRetries === 1
|
|
10155
|
+
? { ...checked.value, internalAttempts: checked.attempts }
|
|
10156
|
+
: checked.value;
|
|
9931
10157
|
}
|
|
9932
10158
|
return { skipped: `no parser for '${ext}' files — parse-check unavailable` };
|
|
9933
10159
|
}
|
|
@@ -9935,7 +10161,7 @@ function parseCheckMutatedFile(absFile, text) {
|
|
|
9935
10161
|
return { skipped: `parse-check errored: ${String(e.message).slice(0, 120)}` };
|
|
9936
10162
|
}
|
|
9937
10163
|
}
|
|
9938
|
-
function cmdMutationGate(options, flags, cwd, write) {
|
|
10164
|
+
function cmdMutationGate(options, flags, cwd, write, injectedRunner) {
|
|
9939
10165
|
const json = flags.has('json');
|
|
9940
10166
|
const fail = (what) => {
|
|
9941
10167
|
write(json ? JSON.stringify({ error: what, exitCode: 2 }) : `dz mutation-gate: ${what}`);
|
|
@@ -10003,6 +10229,7 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10003
10229
|
const results = [];
|
|
10004
10230
|
const observations = [];
|
|
10005
10231
|
const warnings = [];
|
|
10232
|
+
const internalRetries = [];
|
|
10006
10233
|
let baseline;
|
|
10007
10234
|
try {
|
|
10008
10235
|
if (gitTop !== null && gitTop !== pkgDir && resolve(pkgDir).startsWith(resolve(gitTop) + sep)) {
|
|
@@ -10046,7 +10273,10 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10046
10273
|
// write below is asserted to RESOLVE inside this root before it happens.
|
|
10047
10274
|
const realScratchRoot = realpathSync(copyDir);
|
|
10048
10275
|
const requireCompletionReceipt = parsed.registry.requireCompletionReceipt === true;
|
|
10049
|
-
const
|
|
10276
|
+
const invokeSuite = () => {
|
|
10277
|
+
if (injectedRunner !== undefined) {
|
|
10278
|
+
return injectedRunner(testCmd, { cwd: copyDir, timeoutMs: timeout });
|
|
10279
|
+
}
|
|
10050
10280
|
const run = spawnSync(testCmd, {
|
|
10051
10281
|
cwd: copyDir,
|
|
10052
10282
|
shell: true,
|
|
@@ -10058,6 +10288,12 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10058
10288
|
const errorCode = run.error && 'code' in run.error && typeof run.error.code === 'string'
|
|
10059
10289
|
? run.error.code
|
|
10060
10290
|
: undefined;
|
|
10291
|
+
// Node may populate both `error` and a numeric `status` for an internal spawn failure. The
|
|
10292
|
+
// error wins except for the two already-named resource observations: a status alongside
|
|
10293
|
+
// EPERM/Unreachable-code is not a suite verdict and takes the one-retry internal-error path.
|
|
10294
|
+
if (run.error !== undefined && errorCode !== 'ETIMEDOUT' && errorCode !== 'ENOBUFS') {
|
|
10295
|
+
throw run.error;
|
|
10296
|
+
}
|
|
10061
10297
|
const signal = typeof run.signal === 'string' ? run.signal : undefined;
|
|
10062
10298
|
let failureReason;
|
|
10063
10299
|
if (typeof run.status !== 'number') {
|
|
@@ -10074,25 +10310,59 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10074
10310
|
}
|
|
10075
10311
|
return {
|
|
10076
10312
|
exitCode: typeof run.status === 'number' ? run.status : null,
|
|
10077
|
-
// Receipt markers may be on stderr. Preserve both streams even on exit 0; stdout-only
|
|
10078
|
-
// collection would silently lose a green-run marker.
|
|
10079
10313
|
output: `${String(run.stdout ?? '')}\n${String(run.stderr ?? '')}`,
|
|
10080
10314
|
...(failureReason !== undefined ? { failureReason } : {}),
|
|
10081
10315
|
};
|
|
10082
10316
|
};
|
|
10317
|
+
const runSuite = (phase, entryId) => {
|
|
10318
|
+
const retried = runWithOneInternalRetry(invokeSuite);
|
|
10319
|
+
const loggedAttempts = retried.attempts.map((attempt) => {
|
|
10320
|
+
if (attempt.outcome !== 'completed' || retried.value === null)
|
|
10321
|
+
return attempt;
|
|
10322
|
+
const outcome = retried.value.exitCode === null
|
|
10323
|
+
? `no exit code (${retried.value.failureReason ?? 'unnamed failure'})`
|
|
10324
|
+
: `exit ${retried.value.exitCode}`;
|
|
10325
|
+
return { ...attempt, detail: `attempt ${attempt.attempt}: completed — ${outcome}` };
|
|
10326
|
+
});
|
|
10327
|
+
if (retried.internalRetries === 1) {
|
|
10328
|
+
const record = entryId === undefined
|
|
10329
|
+
? { phase, attempts: loggedAttempts }
|
|
10330
|
+
: { phase, entryId, attempts: loggedAttempts };
|
|
10331
|
+
internalRetries.push(record);
|
|
10332
|
+
if (!json)
|
|
10333
|
+
write(`mutation-gate: internal retry — ${loggedAttempts.map((attempt) => attempt.detail).join('; ')}`);
|
|
10334
|
+
}
|
|
10335
|
+
const internalAttemptLog = retried.internalRetries === 1
|
|
10336
|
+
? loggedAttempts.map((attempt) => attempt.detail).join('; ')
|
|
10337
|
+
: undefined;
|
|
10338
|
+
if (retried.value !== null) {
|
|
10339
|
+
return {
|
|
10340
|
+
...retried.value,
|
|
10341
|
+
...(internalAttemptLog !== undefined ? { internalAttemptLog } : {}),
|
|
10342
|
+
};
|
|
10343
|
+
}
|
|
10344
|
+
return {
|
|
10345
|
+
exitCode: null,
|
|
10346
|
+
output: '',
|
|
10347
|
+
failureReason: retried.failureReason ?? 'runner-internal-error: persistent after 2/2 attempts',
|
|
10348
|
+
...(internalAttemptLog !== undefined ? { internalAttemptLog } : {}),
|
|
10349
|
+
};
|
|
10350
|
+
};
|
|
10083
10351
|
// Baseline BEFORE any mutation: a red copy proves nothing, and reading it as a mutation
|
|
10084
10352
|
// result would be this gate shipping the defect class it exists to catch.
|
|
10085
10353
|
if (!json)
|
|
10086
10354
|
write(`mutation-gate: baseline suite in scratch copy of ${pkgDir} …`);
|
|
10087
|
-
const base = runSuite();
|
|
10088
|
-
baseline = classifyBaseline(base.exitCode, base.failureReason
|
|
10355
|
+
const base = runSuite('baseline');
|
|
10356
|
+
baseline = classifyBaseline(base.exitCode, base.failureReason, base.exitCode !== null && base.exitCode !== 0
|
|
10357
|
+
? attributeBaselineRedness(base.output, entries.map((entry) => entry.file))
|
|
10358
|
+
: undefined);
|
|
10089
10359
|
if (!baseline.ok) {
|
|
10090
10360
|
if (json) {
|
|
10091
|
-
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, baseline, results: [], exitCode:
|
|
10092
|
-
return
|
|
10361
|
+
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, baseline, results: [], internalRetries, exitCode: 1 }, null, 2));
|
|
10362
|
+
return 1;
|
|
10093
10363
|
}
|
|
10094
10364
|
write(renderMutationReport([], baseline, pkgDir));
|
|
10095
|
-
return
|
|
10365
|
+
return 1;
|
|
10096
10366
|
}
|
|
10097
10367
|
for (const entry of entries) {
|
|
10098
10368
|
const filePath = join(copyDir, entry.file);
|
|
@@ -10135,6 +10405,8 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10135
10405
|
write(`mutation-gate: ${entry.id} — mutating ${entry.file}, running suite …`);
|
|
10136
10406
|
let run = null;
|
|
10137
10407
|
let parseError;
|
|
10408
|
+
let parseInternalFailureReason;
|
|
10409
|
+
let parseInternalAttemptLog;
|
|
10138
10410
|
try {
|
|
10139
10411
|
writeFileSync(filePath, applied.text);
|
|
10140
10412
|
// Route-a guard: the mutated file must still PARSE — a load failure reddens the whole
|
|
@@ -10145,11 +10417,18 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10145
10417
|
if (!json)
|
|
10146
10418
|
write(`mutation-gate: WARNING ${entry.id}: parse-check skipped — ${check.skipped}`);
|
|
10147
10419
|
}
|
|
10420
|
+
if (check.internalAttempts !== undefined) {
|
|
10421
|
+
internalRetries.push({ phase: 'parse-check', entryId: entry.id, attempts: check.internalAttempts });
|
|
10422
|
+
parseInternalAttemptLog = check.internalAttempts.map((attempt) => attempt.detail).join('; ');
|
|
10423
|
+
if (!json)
|
|
10424
|
+
write(`mutation-gate: internal retry — ${parseInternalAttemptLog}`);
|
|
10425
|
+
}
|
|
10426
|
+
parseInternalFailureReason = check.internalFailureReason;
|
|
10148
10427
|
if (check.error !== undefined) {
|
|
10149
10428
|
parseError = check.error; // no suite run: the verdict is MUTATION_UNPARSEABLE regardless
|
|
10150
10429
|
}
|
|
10151
|
-
else {
|
|
10152
|
-
run = runSuite();
|
|
10430
|
+
else if (parseInternalFailureReason === undefined) {
|
|
10431
|
+
run = runSuite('mutation', entry.id);
|
|
10153
10432
|
}
|
|
10154
10433
|
}
|
|
10155
10434
|
finally {
|
|
@@ -10185,14 +10464,24 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10185
10464
|
// those verdicts outrank the rebaseline check, so the extra suite run would buy nothing.
|
|
10186
10465
|
let rebaselineExitCode;
|
|
10187
10466
|
let rebaselineFailureReason;
|
|
10467
|
+
let rebaselineAttribution;
|
|
10468
|
+
let rebaselineInternalAttemptLog;
|
|
10188
10469
|
if (rebaselineMode === 'per-entry' && run !== null && run.exitCode !== null && run.exitCode !== 0
|
|
10189
10470
|
&& fileLoadFailure === undefined && outputUnrecognised === undefined && receiptMismatch === undefined) {
|
|
10190
10471
|
if (!json)
|
|
10191
10472
|
write(`mutation-gate: ${entry.id} — re-baselining the restored tree …`);
|
|
10192
|
-
const rebaselineRun = runSuite();
|
|
10473
|
+
const rebaselineRun = runSuite('rebaseline', entry.id);
|
|
10193
10474
|
rebaselineExitCode = rebaselineRun.exitCode;
|
|
10194
10475
|
rebaselineFailureReason = rebaselineRun.failureReason;
|
|
10476
|
+
rebaselineInternalAttemptLog = rebaselineRun.internalAttemptLog;
|
|
10477
|
+
if (rebaselineRun.exitCode !== null && rebaselineRun.exitCode !== 0) {
|
|
10478
|
+
rebaselineAttribution = attributeBaselineRedness(rebaselineRun.output, entries.map((candidate) => candidate.file));
|
|
10479
|
+
}
|
|
10195
10480
|
}
|
|
10481
|
+
const entryRunFailureReason = run?.failureReason ?? parseInternalFailureReason;
|
|
10482
|
+
const entryInternalAttemptLog = [parseInternalAttemptLog, run?.internalAttemptLog, rebaselineInternalAttemptLog]
|
|
10483
|
+
.filter((log) => log !== undefined)
|
|
10484
|
+
.join('; ');
|
|
10196
10485
|
const obs = {
|
|
10197
10486
|
entry,
|
|
10198
10487
|
occurrences: 1,
|
|
@@ -10202,9 +10491,11 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10202
10491
|
...(fileLoadFailure !== undefined ? { fileLoadFailure } : {}),
|
|
10203
10492
|
...(outputUnrecognised !== undefined ? { outputUnrecognised } : {}),
|
|
10204
10493
|
...(receiptMismatch !== undefined ? { receiptMismatch } : {}),
|
|
10205
|
-
...(
|
|
10494
|
+
...(entryRunFailureReason !== undefined ? { runFailureReason: entryRunFailureReason } : {}),
|
|
10495
|
+
...(entryInternalAttemptLog !== '' ? { internalAttemptLog: entryInternalAttemptLog } : {}),
|
|
10206
10496
|
...(rebaselineExitCode !== undefined ? { rebaselineExitCode } : {}),
|
|
10207
10497
|
...(rebaselineFailureReason !== undefined ? { rebaselineFailureReason } : {}),
|
|
10498
|
+
...(rebaselineAttribution !== undefined ? { rebaselineAttribution } : {}),
|
|
10208
10499
|
};
|
|
10209
10500
|
observations.push(obs);
|
|
10210
10501
|
results.push(classifyMutationOutcome(obs));
|
|
@@ -10217,7 +10508,7 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10217
10508
|
if (rebaselineMode === 'final') {
|
|
10218
10509
|
if (!json)
|
|
10219
10510
|
write('mutation-gate: final re-baseline of the restored tree …');
|
|
10220
|
-
const finalRun = runSuite();
|
|
10511
|
+
const finalRun = runSuite('final-rebaseline');
|
|
10221
10512
|
const finalExit = finalRun.exitCode;
|
|
10222
10513
|
if (finalExit !== 0) {
|
|
10223
10514
|
const what = finalExit === null ? `no exit code: ${finalRun.failureReason ?? 'unknown timeout / spawn failure'}` : `exit ${finalExit}`;
|
|
@@ -10228,6 +10519,12 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10228
10519
|
...obs,
|
|
10229
10520
|
rebaselineExitCode: finalExit,
|
|
10230
10521
|
...(finalRun.failureReason !== undefined ? { rebaselineFailureReason: finalRun.failureReason } : {}),
|
|
10522
|
+
...(finalRun.internalAttemptLog !== undefined
|
|
10523
|
+
? { internalAttemptLog: [obs.internalAttemptLog, finalRun.internalAttemptLog].filter((log) => log !== undefined).join('; ') }
|
|
10524
|
+
: {}),
|
|
10525
|
+
...(finalExit !== null && finalExit !== 0
|
|
10526
|
+
? { rebaselineAttribution: attributeBaselineRedness(finalRun.output, entries.map((entry) => entry.file)) }
|
|
10527
|
+
: {}),
|
|
10231
10528
|
}));
|
|
10232
10529
|
results.length = 0;
|
|
10233
10530
|
results.push(...reclassified);
|
|
@@ -10247,7 +10544,7 @@ function cmdMutationGate(options, flags, cwd, write) {
|
|
|
10247
10544
|
}
|
|
10248
10545
|
const exitCode = mutationGateExitCode(results, baseline.ok);
|
|
10249
10546
|
if (json) {
|
|
10250
|
-
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, rebaselineMode, baseline, results, summary: summarizeMutationResults(results), warnings, exitCode }, null, 2));
|
|
10547
|
+
write(JSON.stringify({ packageDir: pkgDir, registryPath, testCommand: testCmd, rebaselineMode, baseline, results, summary: summarizeMutationResults(results), warnings, internalRetries, exitCode }, null, 2));
|
|
10251
10548
|
return exitCode;
|
|
10252
10549
|
}
|
|
10253
10550
|
write(renderMutationReport(results, baseline, pkgDir));
|
|
@@ -12091,6 +12388,18 @@ function cmdFeatureAdrRecord(options, flags, cwd, write) {
|
|
|
12091
12388
|
targetExists: existsSync(target),
|
|
12092
12389
|
targetHasPair: flags.has('once') && existsSync(target),
|
|
12093
12390
|
timestamp: new Date().toISOString(),
|
|
12391
|
+
// WHO ran it: `--runner <id>` when the caller knows, otherwise this host. The workflow cannot
|
|
12392
|
+
// supply it — it has no host inside its sandbox — so the identity is resolved here, at the one
|
|
12393
|
+
// seam that runs outside. hostname() can throw on an exotic setup; an unresolvable runner stays
|
|
12394
|
+
// ABSENT rather than becoming the string 'unknown', which would later join as if it were one.
|
|
12395
|
+
runnerId: (options.get('runner') ?? '').trim() !== ''
|
|
12396
|
+
? (options.get('runner') ?? '').trim()
|
|
12397
|
+
: (() => { try {
|
|
12398
|
+
return hostname();
|
|
12399
|
+
}
|
|
12400
|
+
catch {
|
|
12401
|
+
return null;
|
|
12402
|
+
} })(),
|
|
12094
12403
|
});
|
|
12095
12404
|
if (decision.line === null)
|
|
12096
12405
|
return emit(decision);
|
|
@@ -13443,6 +13752,143 @@ async function cmdQeBridge(options, flags, cwd, write) {
|
|
|
13443
13752
|
cleanupIsolated();
|
|
13444
13753
|
}
|
|
13445
13754
|
}
|
|
13755
|
+
function scoreReceiptFiles(root) {
|
|
13756
|
+
const featuresDir = join(root, 'features');
|
|
13757
|
+
let features;
|
|
13758
|
+
try {
|
|
13759
|
+
features = readdirSync(featuresDir, { withFileTypes: true });
|
|
13760
|
+
}
|
|
13761
|
+
catch {
|
|
13762
|
+
return [];
|
|
13763
|
+
}
|
|
13764
|
+
const receipts = [];
|
|
13765
|
+
for (const feature of features) {
|
|
13766
|
+
if (!feature.isDirectory())
|
|
13767
|
+
continue;
|
|
13768
|
+
const stateDir = join(featuresDir, feature.name, '.fa-state');
|
|
13769
|
+
let entries;
|
|
13770
|
+
try {
|
|
13771
|
+
if (lstatSync(stateDir).isSymbolicLink())
|
|
13772
|
+
continue;
|
|
13773
|
+
entries = readdirSync(stateDir, { withFileTypes: true });
|
|
13774
|
+
}
|
|
13775
|
+
catch {
|
|
13776
|
+
continue;
|
|
13777
|
+
}
|
|
13778
|
+
for (const entry of entries) {
|
|
13779
|
+
if (!entry.isFile())
|
|
13780
|
+
continue;
|
|
13781
|
+
const match = /^score-(.+)\.json$/.exec(entry.name);
|
|
13782
|
+
if (match === null || match[1] === undefined || match[1] === '')
|
|
13783
|
+
continue;
|
|
13784
|
+
const path = join(stateDir, entry.name);
|
|
13785
|
+
receipts.push({ path, displayPath: relative(root, path), qeHash: match[1] });
|
|
13786
|
+
}
|
|
13787
|
+
}
|
|
13788
|
+
return receipts.sort((a, b) => a.displayPath < b.displayPath ? -1 : a.displayPath > b.displayPath ? 1 : 0);
|
|
13789
|
+
}
|
|
13790
|
+
function scoreAggregateChainLine(text) {
|
|
13791
|
+
if (text === '') {
|
|
13792
|
+
return { line: 'chain: NOT_PRESENT — no aggregate evidence file was created', verification: null, defectAges: null };
|
|
13793
|
+
}
|
|
13794
|
+
const verification = verifyEventChainText(text);
|
|
13795
|
+
const defectAges = classifyChainDefects(verification, verification.lines);
|
|
13796
|
+
const kinds = new Map();
|
|
13797
|
+
for (const defect of verification.defects)
|
|
13798
|
+
kinds.set(defect.kind, (kinds.get(defect.kind) ?? 0) + 1);
|
|
13799
|
+
const kindText = [...kinds.entries()].map(([kind, count]) => `${kind}: ${count}`).join(' · ');
|
|
13800
|
+
const line = `chain: ${verification.ok ? 'OK' : 'FAILED'} · ${verification.chained} chained · ` +
|
|
13801
|
+
`${verification.resets} recorded restart(s) · before-run defects ${defectAges.beforeRun.length} · ` +
|
|
13802
|
+
`in-run defects ${defectAges.inRun.length} · current run ${defectAges.runRecords} record(s)` +
|
|
13803
|
+
(kindText === '' ? '' : ` · ${kindText}`) +
|
|
13804
|
+
` — ${verification.scope}`;
|
|
13805
|
+
return { line, verification, defectAges };
|
|
13806
|
+
}
|
|
13807
|
+
function cmdScoreAll(options, flags, cwd, write) {
|
|
13808
|
+
const json = flags.has('json');
|
|
13809
|
+
if (options.has('slug')) {
|
|
13810
|
+
write(json
|
|
13811
|
+
? JSON.stringify({ error: '--all and --slug are mutually exclusive', exitCode: 1 })
|
|
13812
|
+
: 'dz score: --all and --slug are mutually exclusive');
|
|
13813
|
+
return 1;
|
|
13814
|
+
}
|
|
13815
|
+
const root = resolve(cwd, options.get('project') ?? '.');
|
|
13816
|
+
const receiptFiles = scoreReceiptFiles(root);
|
|
13817
|
+
if (receiptFiles.length === 0) {
|
|
13818
|
+
const report = buildScoreAggregateReport([], [], 0);
|
|
13819
|
+
const chain = scoreAggregateChainLine('');
|
|
13820
|
+
if (json)
|
|
13821
|
+
write(JSON.stringify({ ...report, chain: null, aggregatePath: '.dz/feature-adr/scorecards.jsonl', exitCode: 0 }, null, 2));
|
|
13822
|
+
else {
|
|
13823
|
+
write(renderScoreAggregateReport(report));
|
|
13824
|
+
write(chain.line);
|
|
13825
|
+
}
|
|
13826
|
+
return 0;
|
|
13827
|
+
}
|
|
13828
|
+
const ts = new Date().toISOString();
|
|
13829
|
+
const rows = [];
|
|
13830
|
+
const unreadableReceipts = [];
|
|
13831
|
+
for (const receipt of receiptFiles) {
|
|
13832
|
+
try {
|
|
13833
|
+
rows.push(scoreReceiptToAggregateRow({
|
|
13834
|
+
content: readFileSync(receipt.path, 'utf8'),
|
|
13835
|
+
qeHash: receipt.qeHash,
|
|
13836
|
+
ts,
|
|
13837
|
+
}));
|
|
13838
|
+
}
|
|
13839
|
+
catch {
|
|
13840
|
+
unreadableReceipts.push(receipt.displayPath);
|
|
13841
|
+
}
|
|
13842
|
+
}
|
|
13843
|
+
const storeDir = join(root, '.dz', 'feature-adr');
|
|
13844
|
+
const aggregatePath = join(storeDir, 'scorecards.jsonl');
|
|
13845
|
+
let finalText = '';
|
|
13846
|
+
let finalRows = rows;
|
|
13847
|
+
let appended = 0;
|
|
13848
|
+
let storeError = null;
|
|
13849
|
+
try {
|
|
13850
|
+
const result = withNamedLockSync(storeDir, 'scorecards', () => {
|
|
13851
|
+
let existingText = '';
|
|
13852
|
+
try {
|
|
13853
|
+
existingText = readFileSync(aggregatePath, 'utf8');
|
|
13854
|
+
}
|
|
13855
|
+
catch (error) {
|
|
13856
|
+
if (error.code !== 'ENOENT')
|
|
13857
|
+
throw error;
|
|
13858
|
+
}
|
|
13859
|
+
const fresh = dedupeScoreAggregateRows(rows, readScoreAggregateRows(existingText));
|
|
13860
|
+
const appendText = appendChainedLines(fresh, readTailInfo(existingText));
|
|
13861
|
+
if (appendText !== '')
|
|
13862
|
+
appendFileSync(aggregatePath, appendText, { encoding: 'utf8', mode: 0o600 });
|
|
13863
|
+
const settledText = existingText + appendText;
|
|
13864
|
+
return { text: settledText, rows: readScoreAggregateRows(settledText), appended: fresh.length };
|
|
13865
|
+
});
|
|
13866
|
+
finalText = result.text;
|
|
13867
|
+
finalRows = result.rows;
|
|
13868
|
+
appended = result.appended;
|
|
13869
|
+
}
|
|
13870
|
+
catch (error) {
|
|
13871
|
+
storeError = error instanceof Error ? error.message : String(error);
|
|
13872
|
+
}
|
|
13873
|
+
const report = buildScoreAggregateReport(finalRows, unreadableReceipts, appended);
|
|
13874
|
+
const chain = scoreAggregateChainLine(finalText);
|
|
13875
|
+
if (json) {
|
|
13876
|
+
write(JSON.stringify({
|
|
13877
|
+
...report,
|
|
13878
|
+
aggregatePath: '.dz/feature-adr/scorecards.jsonl',
|
|
13879
|
+
chain: chain.verification === null ? null : { verification: chain.verification, defectAges: chain.defectAges },
|
|
13880
|
+
storeError,
|
|
13881
|
+
exitCode: 0,
|
|
13882
|
+
}, null, 2));
|
|
13883
|
+
}
|
|
13884
|
+
else {
|
|
13885
|
+
write(renderScoreAggregateReport(report));
|
|
13886
|
+
write(chain.line);
|
|
13887
|
+
if (storeError !== null)
|
|
13888
|
+
write(`store error (nothing was claimed appended): ${storeError}`);
|
|
13889
|
+
}
|
|
13890
|
+
return 0;
|
|
13891
|
+
}
|
|
13446
13892
|
function cmdScore(options, flags, cwd, write) {
|
|
13447
13893
|
const json = flags.has('json');
|
|
13448
13894
|
if (flags.has('help')) {
|
|
@@ -13451,13 +13897,14 @@ function cmdScore(options, flags, cwd, write) {
|
|
|
13451
13897
|
write(JSON.stringify({ help: usage, exitCode: 0 })); // --json stays ONE document even for help
|
|
13452
13898
|
else {
|
|
13453
13899
|
write(usage);
|
|
13900
|
+
write('dz score --all [--project <dir>] [--json] — sweep immutable score receipts into the append-only chained aggregate');
|
|
13454
13901
|
write(' disciplines: ADR confirmation · discrimination · cross-model QE · live verification · README-first · learning loop · amendments');
|
|
13455
13902
|
write(' descriptive-only, never a gate: a low score exits 0');
|
|
13456
13903
|
}
|
|
13457
13904
|
return 0;
|
|
13458
13905
|
}
|
|
13459
13906
|
for (const flag of flags) {
|
|
13460
|
-
if (!new Set(['json', 'help']).has(flag)) {
|
|
13907
|
+
if (!new Set(['json', 'help', 'all']).has(flag)) {
|
|
13461
13908
|
write(json ? JSON.stringify({ error: `unknown option --${flag}`, exitCode: 1 }) : `dz score: unknown option --${flag}\n allowed: --slug <feature>, --project <dir>, --json`);
|
|
13462
13909
|
return 1;
|
|
13463
13910
|
}
|
|
@@ -13469,6 +13916,8 @@ function cmdScore(options, flags, cwd, write) {
|
|
|
13469
13916
|
return 1;
|
|
13470
13917
|
}
|
|
13471
13918
|
}
|
|
13919
|
+
if (flags.has('all'))
|
|
13920
|
+
return cmdScoreAll(options, flags, cwd, write);
|
|
13472
13921
|
const slug = options.get('slug') ?? '';
|
|
13473
13922
|
// The delivery-check traversal lesson, upgraded to a WHITELIST: `.` slipped the blacklist and
|
|
13474
13923
|
// silently aggregated the entire features/ tree as one "run" (Codex QE #2).
|
|
@@ -15342,17 +15791,19 @@ function cmdStats(cwd, write) {
|
|
|
15342
15791
|
}
|
|
15343
15792
|
const dirs = readdirSync(baseDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
15344
15793
|
const packages = dirs.length;
|
|
15345
|
-
|
|
15346
|
-
|
|
15347
|
-
|
|
15348
|
-
|
|
15349
|
-
|
|
15350
|
-
|
|
15351
|
-
|
|
15352
|
-
|
|
15353
|
-
|
|
15354
|
-
|
|
15355
|
-
|
|
15794
|
+
// Backlog e160aeee. This used to walk the tree ITSELF, and was wrong in two independent ways:
|
|
15795
|
+
// it counted only packages whose NAME starts with `skills-` (health-advisor, p-replicator,
|
|
15796
|
+
// keysarium and trip-planner were therefore invisible), and it knew only ONE of the three skill
|
|
15797
|
+
// layouts. Result: 203 here against 250 from `dz registry` on the same tree — two counters of one
|
|
15798
|
+
// quantity, each unable to refute the other because neither knew the other existed.
|
|
15799
|
+
//
|
|
15800
|
+
// The fix is structural, not arithmetic: there is now ONE enumerator, and both commands ask it.
|
|
15801
|
+
// Pinned by test/stats-registry-parity.test.ts, whose red half is this exact divergence.
|
|
15802
|
+
// The registry already PUBLISHES these totals; recomputing them from `entries` here would be a
|
|
15803
|
+
// third implementation of the same count, which is the very defect being fixed.
|
|
15804
|
+
const registry = buildRegistry(cwd);
|
|
15805
|
+
const totalSkills = registry.totalSkills;
|
|
15806
|
+
const skillPacks = registry.totalPacks;
|
|
15356
15807
|
const targets = TARGET_NAMES.length;
|
|
15357
15808
|
const presets = PRESET_NAMES.length;
|
|
15358
15809
|
write(`dz stats — DZ Harness Hub`);
|
|
@@ -15837,7 +16288,28 @@ export async function runCli(argv, io = {}) {
|
|
|
15837
16288
|
// let a guard treat "I could not tell you" as "I answered you".
|
|
15838
16289
|
return version === 'unknown' ? 1 : 0;
|
|
15839
16290
|
}
|
|
15840
|
-
|
|
16291
|
+
// `-h` is the most-typed help flag and is NOT a command: before the unknown-command contract
|
|
16292
|
+
// landed it fell through to the switch and still printed usage; afterwards it would have died
|
|
16293
|
+
// with exit 2 and an empty stdout (measured regression, cross-model QE M1). It belongs beside
|
|
16294
|
+
// `-v` above — an argv-level flag, resolved before command dispatch.
|
|
16295
|
+
if (argv[0] === '-h') {
|
|
16296
|
+
write(USAGE);
|
|
16297
|
+
return 0;
|
|
16298
|
+
}
|
|
16299
|
+
// A bare `--typo` leaves the command empty, so the usage branch reported SUCCESS on a misspelled
|
|
16300
|
+
// FLAG exactly as it used to on a misspelled VERB (cross-model QE M2): `dz --frobnicate` exited 0
|
|
16301
|
+
// with 30 KB of usage. The refusal is deliberately narrowed to the no-command case, because the
|
|
16302
|
+
// warn-don't-refuse decision above is measured and still stands: with a command present, an
|
|
16303
|
+
// unrecognised name may simply be missing from KNOWN_CLI_FLAGS and refusing would break working
|
|
16304
|
+
// invocations. With NO command there is nothing the flag could belong to, so it is a usage error.
|
|
16305
|
+
if (command === '') {
|
|
16306
|
+
const strayNames = unknownFlagNotice([...flags, ...options.keys()].filter((k) => !k.startsWith('_positional_')), KNOWN_CLI_FLAGS).map((n) => n.name);
|
|
16307
|
+
if (strayNames.length > 0) {
|
|
16308
|
+
writeErr(`dz: unknown option --${strayNames[0]} — run 'dz help' for usage`);
|
|
16309
|
+
return 2;
|
|
16310
|
+
}
|
|
16311
|
+
}
|
|
16312
|
+
if (command === '' || command === 'help' || (flags.has('help') && DZ_COMMANDS.includes(command))) {
|
|
15841
16313
|
write(USAGE);
|
|
15842
16314
|
return 0;
|
|
15843
16315
|
}
|
|
@@ -15896,6 +16368,8 @@ export async function runCli(argv, io = {}) {
|
|
|
15896
16368
|
return cmdStatusline(options, flags, cwd, write, readStdin);
|
|
15897
16369
|
case 'usage':
|
|
15898
16370
|
return cmdUsage(options, optionLists, flags, cwd, write);
|
|
16371
|
+
case 'chain':
|
|
16372
|
+
return cmdChain(options, flags, cwd, write);
|
|
15899
16373
|
case 'claim-check':
|
|
15900
16374
|
return cmdClaimCheck(options, optionLists, flags, cwd, write);
|
|
15901
16375
|
case 'lint':
|
|
@@ -15917,7 +16391,7 @@ export async function runCli(argv, io = {}) {
|
|
|
15917
16391
|
case 'diff':
|
|
15918
16392
|
return cmdDiff(options, cwd, write);
|
|
15919
16393
|
case 'recommend':
|
|
15920
|
-
return cmdRecommend(options, cwd, write);
|
|
16394
|
+
return cmdRecommend(options, flags, cwd, write);
|
|
15921
16395
|
case 'upgrade':
|
|
15922
16396
|
return cmdUpgrade(options, flags, cwd, write, writeErr);
|
|
15923
16397
|
case 'auto-canonicalize':
|
|
@@ -15966,8 +16440,22 @@ export async function runCli(argv, io = {}) {
|
|
|
15966
16440
|
return cmdChallenge(options, flags, cwd, write);
|
|
15967
16441
|
case 'discrimination-check':
|
|
15968
16442
|
return cmdDiscriminationCheck(options, flags, cwd, write);
|
|
15969
|
-
case 'mutation-gate':
|
|
15970
|
-
|
|
16443
|
+
case 'mutation-gate': {
|
|
16444
|
+
try {
|
|
16445
|
+
return cmdMutationGate(options, flags, cwd, write, io.mutationGateRunner);
|
|
16446
|
+
}
|
|
16447
|
+
catch (error) {
|
|
16448
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
16449
|
+
const head = Array.from(raw.split(/\r?\n/, 1)[0]?.trim() || 'unknown internal error').slice(0, 160).join('');
|
|
16450
|
+
if (flags.has('json')) {
|
|
16451
|
+
write(JSON.stringify({ verdict: 'INCONCLUSIVE', reason: 'runner-internal-error', error: head, exitCode: 1 }));
|
|
16452
|
+
}
|
|
16453
|
+
else {
|
|
16454
|
+
write(`mutation-gate: INTERNAL ERROR (${head}) — verdict INCONCLUSIVE, exit 1`);
|
|
16455
|
+
}
|
|
16456
|
+
return 1;
|
|
16457
|
+
}
|
|
16458
|
+
}
|
|
15971
16459
|
case 'delivery-check':
|
|
15972
16460
|
return cmdDeliveryCheck(options, flags, cwd, write);
|
|
15973
16461
|
case 'skills-verify':
|
|
@@ -16021,9 +16509,8 @@ export async function runCli(argv, io = {}) {
|
|
|
16021
16509
|
case 'import-ecc':
|
|
16022
16510
|
return await cmdImportEcc(options, flags, cwd, write);
|
|
16023
16511
|
default:
|
|
16024
|
-
|
|
16025
|
-
|
|
16026
|
-
return 1;
|
|
16512
|
+
writeErr(`dz: unknown command ${JSON.stringify(command)} — run 'dz help' for the command list`);
|
|
16513
|
+
return 2;
|
|
16027
16514
|
}
|
|
16028
16515
|
}
|
|
16029
16516
|
catch (error) {
|