@dzhechkov/harness-core 0.8.37 → 0.8.39
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 +253 -133
- package/README.md +209 -0
- package/dist/agentdb-index.d.ts +3 -3
- package/dist/agentdb-index.js +5 -5
- package/dist/agentdb-index.js.map +1 -1
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +4 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/backlog.d.ts +2 -1
- package/dist/backlog.d.ts.map +1 -1
- package/dist/backlog.js +3 -2
- package/dist/backlog.js.map +1 -1
- package/dist/brain.d.ts.map +1 -1
- package/dist/brain.js +9 -2
- package/dist/brain.js.map +1 -1
- package/dist/bto-optimize.d.ts.map +1 -1
- package/dist/bto-optimize.js +9 -12
- package/dist/bto-optimize.js.map +1 -1
- package/dist/claim-check.d.ts.map +1 -1
- package/dist/claim-check.js +50 -14
- package/dist/claim-check.js.map +1 -1
- package/dist/cross-family-control.d.ts +35 -0
- package/dist/cross-family-control.d.ts.map +1 -1
- package/dist/cross-family-control.js +49 -3
- package/dist/cross-family-control.js.map +1 -1
- package/dist/experiment-assign.d.ts +110 -0
- package/dist/experiment-assign.d.ts.map +1 -0
- package/dist/experiment-assign.js +229 -0
- package/dist/experiment-assign.js.map +1 -0
- package/dist/feature-adr-checkpoints.d.ts +7 -2
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +20 -5
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/feature-adr-envelope.d.ts +12 -0
- package/dist/feature-adr-envelope.d.ts.map +1 -1
- package/dist/feature-adr-envelope.js +12 -1
- package/dist/feature-adr-envelope.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +4 -0
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +15 -1
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -4
- package/dist/index.js.map +1 -1
- package/dist/ledger-cost-fill.d.ts +58 -0
- package/dist/ledger-cost-fill.d.ts.map +1 -0
- package/dist/ledger-cost-fill.js +78 -0
- package/dist/ledger-cost-fill.js.map +1 -0
- package/dist/loop-blobs.generated.js +2 -2
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/mutation-gate.d.ts +29 -0
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +20 -0
- package/dist/mutation-gate.js.map +1 -1
- package/dist/name-check.d.ts +20 -1
- package/dist/name-check.d.ts.map +1 -1
- package/dist/name-check.js +42 -1
- package/dist/name-check.js.map +1 -1
- package/dist/no-stubs.d.ts +10 -0
- package/dist/no-stubs.d.ts.map +1 -1
- package/dist/no-stubs.js +13 -9
- package/dist/no-stubs.js.map +1 -1
- package/dist/patterns.d.ts +24 -0
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +49 -9
- package/dist/patterns.js.map +1 -1
- package/dist/publish-source-scope.d.ts +32 -0
- package/dist/publish-source-scope.d.ts.map +1 -0
- package/dist/publish-source-scope.js +53 -0
- package/dist/publish-source-scope.js.map +1 -0
- package/dist/publish.d.ts +5 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +32 -21
- package/dist/publish.js.map +1 -1
- package/dist/qe-bridge.d.ts +12 -1
- package/dist/qe-bridge.d.ts.map +1 -1
- package/dist/qe-bridge.js +21 -11
- package/dist/qe-bridge.js.map +1 -1
- package/dist/rake-analyzer.d.ts +10 -3
- package/dist/rake-analyzer.d.ts.map +1 -1
- package/dist/rake-analyzer.js +84 -22
- package/dist/rake-analyzer.js.map +1 -1
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +8 -4
- package/dist/recap.js.map +1 -1
- package/dist/release.d.ts +2 -1
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +18 -4
- package/dist/release.js.map +1 -1
- package/dist/reqe.d.ts.map +1 -1
- package/dist/reqe.js +11 -1
- package/dist/reqe.js.map +1 -1
- package/dist/review-cost.d.ts +51 -0
- package/dist/review-cost.d.ts.map +1 -0
- package/dist/review-cost.js +110 -0
- package/dist/review-cost.js.map +1 -0
- package/dist/round.d.ts +136 -3
- package/dist/round.d.ts.map +1 -1
- package/dist/round.js +215 -6
- package/dist/round.js.map +1 -1
- package/dist/run-records.d.ts +51 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +148 -2
- package/dist/run-records.js.map +1 -1
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +8 -1
- package/dist/score.js.map +1 -1
- package/dist/sign.d.ts +23 -0
- package/dist/sign.d.ts.map +1 -1
- package/dist/sign.js +52 -0
- package/dist/sign.js.map +1 -1
- package/dist/skills-verify.d.ts +8 -5
- package/dist/skills-verify.d.ts.map +1 -1
- package/dist/skills-verify.js +59 -7
- package/dist/skills-verify.js.map +1 -1
- package/dist/store-guard-prune.d.ts +22 -0
- package/dist/store-guard-prune.d.ts.map +1 -0
- package/dist/store-guard-prune.js +54 -0
- package/dist/store-guard-prune.js.map +1 -0
- package/dist/sweep-failure-classify.d.ts +45 -0
- package/dist/sweep-failure-classify.d.ts.map +1 -0
- package/dist/sweep-failure-classify.js +90 -0
- package/dist/sweep-failure-classify.js.map +1 -0
- package/dist/workflow-run-dispatch.d.ts +14 -2
- package/dist/workflow-run-dispatch.d.ts.map +1 -1
- package/dist/workflow-run-dispatch.js +14 -2
- package/dist/workflow-run-dispatch.js.map +1 -1
- package/package.json +1 -1
- package/sbom.json +432 -132
- package/src/agentdb-index.ts +5 -5
- package/src/amendment-trace.ts +4 -1
- package/src/backlog.ts +3 -2
- package/src/brain.ts +8 -2
- package/src/bto-optimize.ts +10 -8
- package/src/claim-check.ts +48 -12
- package/src/cross-family-control.ts +81 -3
- package/src/experiment-assign.ts +276 -0
- package/src/feature-adr-checkpoints.ts +20 -5
- package/src/feature-adr-envelope.ts +24 -1
- package/src/feature-adr-routing.ts +15 -1
- package/src/index.ts +23 -4
- package/src/loop-blobs.generated.ts +2 -2
- package/src/mutation-gate.ts +59 -0
- package/src/name-check.ts +43 -1
- package/src/no-stubs.ts +14 -5
- package/src/patterns.ts +49 -8
- package/src/publish-source-scope.ts +53 -0
- package/src/publish.ts +38 -16
- package/src/qe-bridge.ts +30 -11
- package/src/rake-analyzer.ts +75 -20
- package/src/rake-signatures.json +98 -0
- package/src/recap.ts +8 -4
- package/src/release.ts +18 -4
- package/src/reqe.ts +11 -1
- package/src/review-cost.ts +139 -0
- package/src/round.ts +327 -11
- package/src/run-records.ts +175 -1
- package/src/score.ts +8 -1
- package/src/sign.ts +53 -0
- package/src/skills-verify.ts +69 -9
- package/src/store-guard-prune.ts +81 -0
- package/src/sweep-failure-classify.ts +88 -0
- package/src/workflow-run-dispatch.ts +14 -2
|
@@ -267,11 +267,11 @@ export function decideCheckpointResume(opts: {
|
|
|
267
267
|
/** Serialize one checkpoint line, or null when the result is null/oversize/unserializable —
|
|
268
268
|
* the caller logs the skip loudly; a missing checkpoint only costs a re-run, never corrupts.
|
|
269
269
|
* A null result is never persisted (Codex QE #8: it would later parse as a resumable entry). */
|
|
270
|
-
export function serializeCheckpoint(stage: string, inputHash: string, result: unknown): string | null {
|
|
270
|
+
export function serializeCheckpoint(stage: string, inputHash: string, result: unknown, ts?: string): string | null {
|
|
271
271
|
if (result === null || result === undefined) return null;
|
|
272
272
|
let line: string;
|
|
273
273
|
try {
|
|
274
|
-
line = JSON.stringify({ stage, inputHash, result });
|
|
274
|
+
line = JSON.stringify({ stage, inputHash, result, ...(ts === undefined ? {} : { ts }) });
|
|
275
275
|
} catch {
|
|
276
276
|
return null;
|
|
277
277
|
}
|
|
@@ -289,6 +289,9 @@ export interface ParsedCheckpointRead {
|
|
|
289
289
|
* command's stdout. */
|
|
290
290
|
export const CHECKPOINT_LS_SENTINEL = '---FA-CKPT-LS---';
|
|
291
291
|
|
|
292
|
+
/** A writer-produced UTC instant. A malformed supplied value is unmeasured, not a bad record. */
|
|
293
|
+
const CHECKPOINT_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
294
|
+
|
|
292
295
|
/** Parse the read-back agent's stdout: JSONL entries (LAST occurrence of a stage wins — a re-run
|
|
293
296
|
* overwrites by append), then the sentinel ON ITS OWN LINE, then one artifact path per line
|
|
294
297
|
* (relative to the feature dir). The sentinel match is LINE-ANCHORED: JSON.stringify never emits
|
|
@@ -308,7 +311,11 @@ export function parseCheckpointRead(text: string): ParsedCheckpointRead {
|
|
|
308
311
|
try {
|
|
309
312
|
const e = JSON.parse(t) as CheckpointEntry;
|
|
310
313
|
if (e && typeof e === 'object' && typeof e.stage === 'string' && typeof e.inputHash === 'string' && 'result' in e && e.result !== null && e.result !== undefined) {
|
|
311
|
-
|
|
314
|
+
const { ts, ...entry } = e;
|
|
315
|
+
// Narrow to `string` before the spread: under exactOptionalPropertyTypes an optional
|
|
316
|
+
// `ts?: string` refuses `string | undefined`, and an unmeasured record must carry NO key
|
|
317
|
+
// rather than an explicit undefined.
|
|
318
|
+
out.entries[e.stage] = typeof ts === 'string' && CHECKPOINT_TIMESTAMP.test(ts) ? { ...entry, ts } : entry;
|
|
312
319
|
} else {
|
|
313
320
|
// last-wins holds for BAD records too: a stage-identifiable null/invalid record ERASES the
|
|
314
321
|
// older entry for that stage instead of silently reactivating it (Codex QE r2 #6).
|
|
@@ -345,11 +352,19 @@ export function checkpointReadCmd(fdirAbs: string): string {
|
|
|
345
352
|
|
|
346
353
|
/** The one Bash command the write agent runs: mkdir the state dir, then append ONE line. The line
|
|
347
354
|
* is single-quote-escaped as a whole — JSON.stringify output never contains literal newlines, so
|
|
348
|
-
* printf '%s\n' emits exactly one record.
|
|
355
|
+
* printf '%s\n' emits exactly one record.
|
|
356
|
+
*
|
|
357
|
+
* The record's `ts` is stamped SHELL-SIDE (`date -u`), the same idiom the decision-recall ledger
|
|
358
|
+
* uses, because the workflow sandbox forbids `Date` and this function must stay pure. A line that
|
|
359
|
+
* is not a JSON object falls back to the plain append: an unstamped record beats a lost one, and
|
|
360
|
+
* the return type stays `string` so no caller grows a new failure path. */
|
|
349
361
|
export function checkpointAppendCmd(fdirAbs: string, line: string): string {
|
|
350
362
|
const dir = shellQuote(fdirAbs + '/.fa-state');
|
|
351
363
|
const file = shellQuote(fdirAbs + '/.fa-state/checkpoints.jsonl');
|
|
352
|
-
|
|
364
|
+
const plain = 'mkdir -p ' + dir + " && printf '%s\\n' " + shellQuote(line) + ' >> ' + file;
|
|
365
|
+
if (typeof line !== 'string' || !line.startsWith('{')) return plain;
|
|
366
|
+
return 'ts=$(date -u +%Y-%m-%dT%H:%M:%SZ); mkdir -p ' + dir
|
|
367
|
+
+ ' && { printf \'{"ts":"%s",\' "$ts"; printf \'%s\\n\' ' + shellQuote(line.slice(1)) + '; } >> ' + file;
|
|
353
368
|
}
|
|
354
369
|
|
|
355
370
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -34,6 +34,18 @@ export interface ExperimentEnvelopeChosen {
|
|
|
34
34
|
* (usage-override, session-inherited fallback) is recorded HERE explicitly — arms stay the set that
|
|
35
35
|
* was actually offered; the winner is never appended to them after the fact. */
|
|
36
36
|
readonly overrides?: Readonly<Record<string, string>>;
|
|
37
|
+
/**
|
|
38
|
+
* ablation-c-start (ADR-001, T3; fix-round-1 BLOCKER #2): the pre-registered ablation-C arm
|
|
39
|
+
* (`direct` | `reference`) for THIS run, filled only when `args.experiment`/`args.taskId` were
|
|
40
|
+
* given AND the workflow successfully RESOLVED an existing assignment for that task via
|
|
41
|
+
* `dz experiment resolve` — never taken from a caller-supplied arm option (that was the BLOCKER
|
|
42
|
+
* fix-round-1 found: a caller could set the arm to anything, with zero journal entry). Deliberately
|
|
43
|
+
* a SEPARATE field from `mode` — `mode` already means "same-family vs cross-family reviewer" (a
|
|
44
|
+
* different axis) — so setting `qeMode` never redefines what `mode` has always meant. Absent (not
|
|
45
|
+
* `null`) when no arm was resolved, so `JSON.stringify` drops the key and an unflagged run's
|
|
46
|
+
* envelope stays byte-identical to before this feature (NFR-2).
|
|
47
|
+
*/
|
|
48
|
+
readonly qeMode?: string;
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
export interface ExperimentEnvelopePolicy {
|
|
@@ -104,7 +116,12 @@ export function buildExperimentEnvelope(input: BuildExperimentEnvelopeInput): Ex
|
|
|
104
116
|
treeSha: input.treeSha,
|
|
105
117
|
treeShaReason: input.treeSha === null ? (input.treeShaReason ?? 'unavailable') : null,
|
|
106
118
|
arms: { mode: [...input.arms.mode], stages: { ...input.arms.stages } },
|
|
107
|
-
chosen: {
|
|
119
|
+
chosen: {
|
|
120
|
+
mode: input.chosen.mode,
|
|
121
|
+
stages: { ...input.chosen.stages },
|
|
122
|
+
overrides: { ...(input.chosen.overrides ?? {}) },
|
|
123
|
+
...(input.chosen.qeMode !== undefined ? { qeMode: input.chosen.qeMode } : {}),
|
|
124
|
+
},
|
|
108
125
|
policy: { ...input.policy },
|
|
109
126
|
evaluator: { ...input.evaluator },
|
|
110
127
|
};
|
|
@@ -217,6 +234,12 @@ export function validateExperimentEnvelope(value: unknown): { ok: true } | { ok:
|
|
|
217
234
|
return { ok: false, reason: `chosen.stages.${stage}: "${String(spec)}" is not a member of arms.stages.${stage} (${offered.join('|')}) and not declared in chosen.overrides` };
|
|
218
235
|
}
|
|
219
236
|
}
|
|
237
|
+
// ablation-c-start (ADR-001, T3): qeMode is OPTIONAL — absent on every run this feature does not
|
|
238
|
+
// touch — but when present must be a non-empty string, same shape rule as every other envelope
|
|
239
|
+
// field (never a silently-accepted empty label).
|
|
240
|
+
if (chosen.qeMode !== undefined && !isNonEmptyString(chosen.qeMode)) {
|
|
241
|
+
return { ok: false, reason: 'chosen.qeMode: expected a non-empty string when present' };
|
|
242
|
+
}
|
|
220
243
|
|
|
221
244
|
if (!isPlainObject(v.policy)) return { ok: false, reason: 'policy: expected an object' };
|
|
222
245
|
const policy = v.policy;
|
|
@@ -1675,6 +1675,10 @@ export function codexDispatchMode(stage: string): CodexDispatch {
|
|
|
1675
1675
|
* SCOPE — `codexReviewCommand` (the diff defines it) and `scopedQePrompt` ("read ONLY these files").
|
|
1676
1676
|
* This constant is retained as a sanity bound on an absurd payload, and is no longer claimed as the
|
|
1677
1677
|
* thing that prevents a stall.
|
|
1678
|
+
*
|
|
1679
|
+
* THIS IS THE ONLY DEFINITION. `workflow-run-dispatch.ts` re-exports it rather than declaring its
|
|
1680
|
+
* own twin (it used to, and nothing compared the two). The direction is forced: this module is
|
|
1681
|
+
* lifted verbatim into the workflow sandbox by `scripts/gen-loop-blobs.mjs`, so it may not import.
|
|
1678
1682
|
*/
|
|
1679
1683
|
export const CODEX_EXEC_PROMPT_CEILING_CHARS = 24_000;
|
|
1680
1684
|
|
|
@@ -2190,7 +2194,11 @@ export function gradeFromReviewFindings(findings: readonly CodexQeFinding[] | nu
|
|
|
2190
2194
|
* The LOCKED decline taxonomy. A `kind` outside this set is a bug, not a new case — which is why
|
|
2191
2195
|
* {@link codexQeDeclineReason} throws on one rather than rendering something plausible.
|
|
2192
2196
|
*/
|
|
2193
|
-
|
|
2197
|
+
// 'scope-not-established' and 'base-ref-not-established' joined the set 2026-09-17 (feature
|
|
2198
|
+
// codex-review-scope + its fix-round-1, finding #7): the workflow's two inline mirrors grew both
|
|
2199
|
+
// kinds; this export copy diverged behind them until now. Appended, never reordered — the SAME
|
|
2200
|
+
// order the two workflow-twin mirrors carry.
|
|
2201
|
+
export const CODEX_QE_DECLINE_KINDS: readonly string[] = ['timeout', 'no-verdict', 'tool-error', 'unusable-output', 'unavailable', 'over-ceiling', 'wrong-tree', 'scope-not-established', 'base-ref-not-established'];
|
|
2194
2202
|
|
|
2195
2203
|
export interface ClassifyCodexQeInput {
|
|
2196
2204
|
readonly exit?: number | null;
|
|
@@ -2350,6 +2358,12 @@ export function codexQeDeclineReason(kind: string | null | undefined, detail?: C
|
|
|
2350
2358
|
if (canonical === 'over-ceiling') {
|
|
2351
2359
|
return 'prompt is ' + chars + ' chars / unscoped — refused before dispatch';
|
|
2352
2360
|
}
|
|
2361
|
+
if (canonical === 'scope-not-established') {
|
|
2362
|
+
return 'review scope NOT ESTABLISHED for uncommitted QE — ' + (d.reason === undefined || d.reason === null || String(d.reason) === '' ? 'the change set could not be measured' : String(d.reason)) + ' — refusing to fall back to --uncommitted on the shared tree';
|
|
2363
|
+
}
|
|
2364
|
+
if (canonical === 'base-ref-not-established') {
|
|
2365
|
+
return 'base ref for the isolated review scope NOT ESTABLISHED — ' + (d.reason === undefined || d.reason === null || String(d.reason) === '' ? 'the base-ref probe failed or was unparseable' : String(d.reason)) + ' — refusing to silently fall back to HEAD';
|
|
2366
|
+
}
|
|
2353
2367
|
throw new Error('codexQeDeclineReason: unknown kind ' + k);
|
|
2354
2368
|
}
|
|
2355
2369
|
|
package/src/index.ts
CHANGED
|
@@ -116,6 +116,7 @@ export * from './trace-bundle.js';
|
|
|
116
116
|
export { BLOBS as LOOP_BLOBS, LOOP_BLOB_NAMES, BLOB_COVERAGE_MANIFEST } from './loop-blobs.generated.js';
|
|
117
117
|
export type { LoopBlob } from './loop-blobs.generated.js';
|
|
118
118
|
export * from './sign.js';
|
|
119
|
+
export * from './publish-source-scope.js';
|
|
119
120
|
export * from './swarm-brief.js';
|
|
120
121
|
export * from './skill-schema.js';
|
|
121
122
|
export { createSkill } from './create-skill.js';
|
|
@@ -144,7 +145,7 @@ export { tokenize, stemToken, stems } from './stem.js';
|
|
|
144
145
|
export * from './package-skill-layouts.js';
|
|
145
146
|
export { recommend } from './recommend.js';
|
|
146
147
|
export { pretrain } from './pretrain.js';
|
|
147
|
-
export { loadPatterns, loadSessions, computePatternBoost, readLearningConfig, readMemoryLearningConfig, BOOST_CAP, recordPattern, recordLessonForms, loadStorePatternsSync, loadStoreRecords, findExactLesson, patternToRecord, recordToPattern, patternRecordId, patternIdentityOf, dreamRecordId, isMirrorableLearning, consolidateSessions, recallPatterns, pruneNoisePatterns, removePatternsByIds, snapshotStore, readReinforcementState, encodeReinforcementState, reinforcePattern, updateReinforcementState, storeStats, lessonDeltaReport, lessonDeltaMap, readQuarantineState, encodeQuarantineState, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns } from './patterns.js';
|
|
148
|
+
export { loadPatterns, loadSessions, computePatternBoost, readLearningConfig, readMemoryLearningConfig, BOOST_CAP, recordPattern, recordLessonForms, loadStorePatternsSync, loadStoreRecords, findExactLesson, patternToRecord, recordToPattern, patternRecordId, patternIdentityOf, dreamRecordId, isMirrorableLearning, consolidateSessions, recallPatterns, pruneNoisePatterns, removePatternsByIds, snapshotStore, readReinforcementState, encodeReinforcementState, reinforcePattern, resolveReinforceTarget, updateReinforcementState, storeStats, lessonDeltaReport, lessonDeltaMap, readQuarantineState, encodeQuarantineState, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns } from './patterns.js';
|
|
148
149
|
export { normalizeLessonForms, validateClassTemplate, lessonPairIdOf, mergeLessonFormHits, mergeLessonMatchedForms } from './lesson-generalization.js';
|
|
149
150
|
export { withStoreLock, withStoreLockSync, storeLockPath, StoreLockTimeoutError, StoreLockCompromisedError, STALE_LOCK_MS, LOCK_TIMEOUT_MS } from './store-lock.js';
|
|
150
151
|
export type { StoreLockOptions } from './store-lock.js';
|
|
@@ -304,6 +305,13 @@ export type {
|
|
|
304
305
|
StoreMarkWriteOptions,
|
|
305
306
|
StoreResetReceipt,
|
|
306
307
|
} from './store-guard.js';
|
|
308
|
+
export { planStoreGuardPrune } from './store-guard-prune.js';
|
|
309
|
+
export type {
|
|
310
|
+
StoreGuardPruneBucket,
|
|
311
|
+
StoreGuardPruneEntry,
|
|
312
|
+
StoreGuardPrunePlan,
|
|
313
|
+
StoreGuardPruneDeps,
|
|
314
|
+
} from './store-guard-prune.js';
|
|
307
315
|
export { statuslineData, readFeatureAdrState, writeFeatureAdrState, featureAdrStateDir, featureAdrStatePath, writeFeatureAdrStateDetailed, renderFeatureAdrPhaseLine } from './statusline.js';
|
|
308
316
|
export type { StatuslineData, StatuslineStoreHealth, FeatureAdrState, WriteFeatureAdrStateInput, WriteFeatureAdrStateResult } from './statusline.js';
|
|
309
317
|
export {
|
|
@@ -501,10 +509,12 @@ export {
|
|
|
501
509
|
} from './amendment-trace.js';
|
|
502
510
|
export {
|
|
503
511
|
RECORD_MAX_LINE_CHARS,
|
|
512
|
+
INCOMPLETE_REASON_CODES,
|
|
504
513
|
decideRecordWrite,
|
|
505
514
|
decideReadBack,
|
|
506
515
|
recordVerdictLine,
|
|
507
516
|
parseModelSpec,
|
|
517
|
+
applyTaskId,
|
|
508
518
|
} from './run-records.js';
|
|
509
519
|
export {
|
|
510
520
|
ENVELOPE_SCHEMA,
|
|
@@ -527,7 +537,7 @@ export type {
|
|
|
527
537
|
} from './feature-adr-envelope.js';
|
|
528
538
|
export { decidePublishSigning, decidePostSigningVerification, decideSignableSet, publishSigningLine, signableSetLine } from './publish-signing.js';
|
|
529
539
|
export type { PublishSigningVerdict, PublishSigningDecision, SignableSetDecision } from './publish-signing.js';
|
|
530
|
-
export type { RecordKind, RecordVerdict, RecordDecision, LedgerEnrichInput, LedgerPriceEntry, ParsedModelSpec } from './run-records.js';
|
|
540
|
+
export type { RecordKind, RecordVerdict, RecordDecision, LedgerEnrichInput, LedgerPriceEntry, ParsedModelSpec, TaskIdLookup, IncompleteReasonCode } from './run-records.js';
|
|
531
541
|
export type { AmendmentAmbiguity, AmendmentRow, AmendmentVerdict, AmendmentResolution, AmendmentOutcome, AmendmentDecision, PlanCoverageGap } from './amendment-trace.js';
|
|
532
542
|
// contract-checklist (ADR-001): pure extraction, canonical rendering, typed report parsing, and
|
|
533
543
|
// exact per-item verification. Filesystem discovery/containment stays in harness-cli.
|
|
@@ -754,7 +764,7 @@ export type {
|
|
|
754
764
|
ChainDefectAges,
|
|
755
765
|
ChainedJournal,
|
|
756
766
|
} from './event-chain.js';
|
|
757
|
-
export { decideProvenance, environmentCanMintProvenance, publishArgv, discoverPackages, publishPackages, bumpPatch, compareVersions, findUnpackagedSkills, findUnpublishedWorkspaceFloors, rewriteWorkspaceSpecs, orderByDependencies, syncReadmeVersion, isChangelogEntryLine, changelogRegion, planReadmeVersionSync } from './publish.js';
|
|
767
|
+
export { decideProvenance, environmentCanMintProvenance, publishArgv, matchesPublishFilter, discoverPackages, publishPackages, bumpPatch, compareVersions, findUnpackagedSkills, findUnpublishedWorkspaceFloors, rewriteWorkspaceSpecs, orderByDependencies, syncReadmeVersion, isChangelogEntryLine, changelogRegion, planReadmeVersionSync } from './publish.js';
|
|
758
768
|
export type { ReadmeVersionSyncPlan, ReadmeSyncRewrite } from './publish.js';
|
|
759
769
|
export { RELEASE_LINE_RE, findReleaseLine, rewriteReleaseLine } from './release-line.js';
|
|
760
770
|
export * from './course-staleness.js';
|
|
@@ -1147,6 +1157,11 @@ export * from './compounding.js';
|
|
|
1147
1157
|
// Advisory command-invocation telemetry + deadwood report (feature dz-deadwood).
|
|
1148
1158
|
export * from './cmd-usage.js';
|
|
1149
1159
|
|
|
1160
|
+
// Deterministic, blocked arm assignment for a prospective ablation (feature ablation-c-start,
|
|
1161
|
+
// ADR-001): pure — no fs, no clock, reuses `mulberry32` (the repo's only PRNG).
|
|
1162
|
+
export { assignArm, readAssignments, verifyAssignmentRecord } from './experiment-assign.js';
|
|
1163
|
+
export type { AssignArmInput, AssignArmResult, AssignmentRecord, ReadAssignmentsResult, VerifyAssignmentResult } from './experiment-assign.js';
|
|
1164
|
+
|
|
1150
1165
|
// Cold-vs-warm EPOCH RUNNER (feature epoch-replay, scout idea #4) — the RESULT leg to compounding's
|
|
1151
1166
|
// readiness leg. Orchestrates + scores; never calls a model. SUPPORTED requires two DISJOINT Wilson
|
|
1152
1167
|
// intervals; INCONCLUSIVE is a first-class honest outcome.
|
|
@@ -1317,8 +1332,12 @@ export * from './run-registry.js';
|
|
|
1317
1332
|
|
|
1318
1333
|
export { JOURNAL_KINDS, formatLine, parseLine, selectWindow, appendWitnessed } from './journal.js';
|
|
1319
1334
|
export type { JournalKind, JournalEvent, JournalLine, JournalIo } from './journal.js';
|
|
1320
|
-
export { openRound, closeRound, listRounds, validateClosedRoundLedgerRow } from './round.js';
|
|
1335
|
+
export { openRound, closeRound, listRounds, validateClosedRoundLedgerRow, readOpenRoundTaskId } from './round.js';
|
|
1321
1336
|
export type { RoundState, RoundExecState, RoundLedgerRow, RoundReviewSidecar } from './round.js';
|
|
1337
|
+
|
|
1338
|
+
// review-cost-ledger T1/FR-1 (ADR-001 п.1): the pure qe-bridge stdout cost parser.
|
|
1339
|
+
export { parseQeBridgeStdoutCost } from './review-cost.js';
|
|
1340
|
+
export type { QeBridgeCost, QeBridgeCostTokens } from './review-cost.js';
|
|
1322
1341
|
export { parseCodexTokens, classifyRoundExecOutcome, buildRoundExecRow } from './round-exec.js';
|
|
1323
1342
|
export type { RoundExecOutcome, RoundExecLedgerRow } from './round-exec.js';
|
|
1324
1343
|
export * from './run-cleanup.js';
|
|
@@ -52,11 +52,11 @@ export const BLOBS: Record<string, LoopBlob> = {
|
|
|
52
52
|
"checkpoints": {
|
|
53
53
|
name: "checkpoints",
|
|
54
54
|
version: "1.2.0",
|
|
55
|
-
contentHash: "
|
|
55
|
+
contentHash: "b602357649042d6b8aa68b5ca5593f7f5c483fdb36a287742e7b27bda8a2a6c7",
|
|
56
56
|
sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-checkpoints.ts",
|
|
57
57
|
requires: [],
|
|
58
58
|
exports: ["CHECKPOINT_STAGES","STAGE_ARTIFACTS","CHECKPOINT_MAX_RESULT_CHARS","CKPT_SCHEMA_VERSION","fnv1a","fnv1a64","checkpointInputHash","resumeMode","decideCheckpointResume","serializeCheckpoint","CHECKPOINT_LS_SENTINEL","parseCheckpointRead","shellQuote","checkpointReadCmd","checkpointAppendCmd","DESIGN_SUBSTAGES","designStageKey","decideDesignFanResume","parseArtifactProbe","ROUTER_CONTRACT_TOKEN"],
|
|
59
|
-
code: "const CHECKPOINT_STAGES = ['router', 'design', 'plan', 'code', 'qe', 'fleet'];\nconst STAGE_ARTIFACTS = {\n router: '00_complexity_assessment.md',\n design: '01_requirements.md',\n plan: '06_implementation_plan.md',\n code: '07_code_changes/change_manifest.md',\n qe: '08_qe_report.md',\n fleet: '09_fleet_qe_assessment.md',\n};\nconst CHECKPOINT_MAX_RESULT_CHARS = 12000;\nconst CKPT_SCHEMA_VERSION = 'fa-ckpt-3';\nconst ROUTER_CONTRACT_TOKEN = 'router-writes-00-v1';\nfunction fnv1a(str) {\n let h = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;\n }\n return h.toString(16).padStart(8, '0');\n}\nfunction fnv1a64(str) {\n return fnv1a(str) + fnv1a('fa-ckpt-salt' + str);\n}\nfunction checkpointInputHash(stage, parts) {\n return fnv1a64(JSON.stringify([CKPT_SCHEMA_VERSION, stage, ...parts.map((p) => (p === undefined ? null : p))]));\n}\nfunction resumeMode(raw) {\n return raw === 'never' ? 'never' : raw === 'force' ? 'force' : 'auto';\n}\nconst DESIGN_SUBSTAGES = ['requirements', 'adr', 'qcsd', 'architecture'];\nfunction designStageKey(sub) {\n return 'design:' + sub;\n}\nfunction decideDesignFanResume(opts) {\n const missingSubstages = [];\n opts.required.forEach((sub, i) => {\n const r = opts.results[i];\n if (r === null || r === undefined)\n missingSubstages.push(sub);\n });\n const missingArtifacts = [];\n let probeMissing = false;\n if (opts.artifacts.length > 0) {\n if (opts.postRunListing === null)\n probeMissing = true;\n else\n for (const rel of opts.artifacts)\n if (!opts.postRunListing.has(rel))\n missingArtifacts.push(rel);\n }\n const reason = missingSubstages.length > 0 ? 'substage-missing'\n : probeMissing ? 'probe-not-established'\n : missingArtifacts.length > 0 ? 'artifact-missing'\n : 'ok';\n return { complete: reason === 'ok', missingSubstages, missingArtifacts, reason };\n}\nfunction decideCheckpointResume(opts) {\n if (opts.mode === 'never')\n return { resume: false, reason: 'mode-never' };\n if (!opts.entry || opts.entry.result === null || opts.entry.result === undefined) {\n return { resume: false, reason: 'no-checkpoint' };\n }\n if (opts.entry.inputHash !== opts.inputHash)\n return { resume: false, reason: 'stale-input' };\n if (opts.mode === 'force')\n return { resume: true, reason: 'resumed-force' };\n const required = opts.artifactRel === null ? [] : (typeof opts.artifactRel === 'string' ? [opts.artifactRel] : opts.artifactRel);\n for (const rel of required) {\n if (!opts.listing.has(rel))\n return { resume: false, reason: 'artifact-missing' };\n }\n return { resume: true, reason: 'resumed' };\n}\nfunction serializeCheckpoint(stage, inputHash, result) {\n if (result === null || result === undefined)\n return null;\n let line;\n try {\n line = JSON.stringify({ stage, inputHash, result });\n }\n catch {\n return null;\n }\n if (typeof line !== 'string' || line.length > CHECKPOINT_MAX_RESULT_CHARS)\n return null;\n return line;\n}\nconst CHECKPOINT_LS_SENTINEL = '---FA-CKPT-LS---';\nfunction parseCheckpointRead(text) {\n const out = { entries: {}, listing: new Set(), malformedLines: 0 };\n const raw = String(text ?? '');\n const lines = raw.split('\\n');\n const sentinelAt = lines.findIndex((l) => l.trim() === CHECKPOINT_LS_SENTINEL);\n const body = sentinelAt === -1 ? lines : lines.slice(0, sentinelAt);\n const ls = sentinelAt === -1 ? [] : lines.slice(sentinelAt + 1);\n for (const line of body) {\n const t = line.trim();\n if (t === '')\n continue;\n try {\n const e = JSON.parse(t);\n if (e && typeof e === 'object' && typeof e.stage === 'string' && typeof e.inputHash === 'string' && 'result' in e && e.result !== null && e.result !== undefined) {\n out.entries[e.stage] =
|
|
59
|
+
code: "const CHECKPOINT_STAGES = ['router', 'design', 'plan', 'code', 'qe', 'fleet'];\nconst STAGE_ARTIFACTS = {\n router: '00_complexity_assessment.md',\n design: '01_requirements.md',\n plan: '06_implementation_plan.md',\n code: '07_code_changes/change_manifest.md',\n qe: '08_qe_report.md',\n fleet: '09_fleet_qe_assessment.md',\n};\nconst CHECKPOINT_MAX_RESULT_CHARS = 12000;\nconst CKPT_SCHEMA_VERSION = 'fa-ckpt-3';\nconst ROUTER_CONTRACT_TOKEN = 'router-writes-00-v1';\nfunction fnv1a(str) {\n let h = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;\n }\n return h.toString(16).padStart(8, '0');\n}\nfunction fnv1a64(str) {\n return fnv1a(str) + fnv1a('fa-ckpt-salt' + str);\n}\nfunction checkpointInputHash(stage, parts) {\n return fnv1a64(JSON.stringify([CKPT_SCHEMA_VERSION, stage, ...parts.map((p) => (p === undefined ? null : p))]));\n}\nfunction resumeMode(raw) {\n return raw === 'never' ? 'never' : raw === 'force' ? 'force' : 'auto';\n}\nconst DESIGN_SUBSTAGES = ['requirements', 'adr', 'qcsd', 'architecture'];\nfunction designStageKey(sub) {\n return 'design:' + sub;\n}\nfunction decideDesignFanResume(opts) {\n const missingSubstages = [];\n opts.required.forEach((sub, i) => {\n const r = opts.results[i];\n if (r === null || r === undefined)\n missingSubstages.push(sub);\n });\n const missingArtifacts = [];\n let probeMissing = false;\n if (opts.artifacts.length > 0) {\n if (opts.postRunListing === null)\n probeMissing = true;\n else\n for (const rel of opts.artifacts)\n if (!opts.postRunListing.has(rel))\n missingArtifacts.push(rel);\n }\n const reason = missingSubstages.length > 0 ? 'substage-missing'\n : probeMissing ? 'probe-not-established'\n : missingArtifacts.length > 0 ? 'artifact-missing'\n : 'ok';\n return { complete: reason === 'ok', missingSubstages, missingArtifacts, reason };\n}\nfunction decideCheckpointResume(opts) {\n if (opts.mode === 'never')\n return { resume: false, reason: 'mode-never' };\n if (!opts.entry || opts.entry.result === null || opts.entry.result === undefined) {\n return { resume: false, reason: 'no-checkpoint' };\n }\n if (opts.entry.inputHash !== opts.inputHash)\n return { resume: false, reason: 'stale-input' };\n if (opts.mode === 'force')\n return { resume: true, reason: 'resumed-force' };\n const required = opts.artifactRel === null ? [] : (typeof opts.artifactRel === 'string' ? [opts.artifactRel] : opts.artifactRel);\n for (const rel of required) {\n if (!opts.listing.has(rel))\n return { resume: false, reason: 'artifact-missing' };\n }\n return { resume: true, reason: 'resumed' };\n}\nfunction serializeCheckpoint(stage, inputHash, result, ts) {\n if (result === null || result === undefined)\n return null;\n let line;\n try {\n line = JSON.stringify({ stage, inputHash, result, ...(ts === undefined ? {} : { ts }) });\n }\n catch {\n return null;\n }\n if (typeof line !== 'string' || line.length > CHECKPOINT_MAX_RESULT_CHARS)\n return null;\n return line;\n}\nconst CHECKPOINT_LS_SENTINEL = '---FA-CKPT-LS---';\nconst CHECKPOINT_TIMESTAMP = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,3})?Z$/;\nfunction parseCheckpointRead(text) {\n const out = { entries: {}, listing: new Set(), malformedLines: 0 };\n const raw = String(text ?? '');\n const lines = raw.split('\\n');\n const sentinelAt = lines.findIndex((l) => l.trim() === CHECKPOINT_LS_SENTINEL);\n const body = sentinelAt === -1 ? lines : lines.slice(0, sentinelAt);\n const ls = sentinelAt === -1 ? [] : lines.slice(sentinelAt + 1);\n for (const line of body) {\n const t = line.trim();\n if (t === '')\n continue;\n try {\n const e = JSON.parse(t);\n if (e && typeof e === 'object' && typeof e.stage === 'string' && typeof e.inputHash === 'string' && 'result' in e && e.result !== null && e.result !== undefined) {\n const { ts, ...entry } = e;\n out.entries[e.stage] = typeof ts === 'string' && CHECKPOINT_TIMESTAMP.test(ts) ? { ...entry, ts } : entry;\n }\n else {\n if (e && typeof e === 'object' && typeof e.stage === 'string')\n delete out.entries[e.stage];\n out.malformedLines++;\n }\n }\n catch {\n out.malformedLines++;\n }\n }\n for (const line of ls) {\n const t = line.trim();\n if (t !== '')\n out.listing.add(t);\n }\n return out;\n}\nfunction shellQuote(s) {\n return \"'\" + String(s).replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction checkpointReadCmd(fdirAbs) {\n const q = shellQuote(fdirAbs);\n return ('cat ' + q + '/.fa-state/checkpoints.jsonl 2>/dev/null || true; ' +\n \"echo '\" + CHECKPOINT_LS_SENTINEL + \"'; \" +\n 'cd ' + q + ' 2>/dev/null && find . -maxdepth 2 -type f 2>/dev/null | sed \"s|^\\\\./||\" || true');\n}\nfunction checkpointAppendCmd(fdirAbs, line) {\n const dir = shellQuote(fdirAbs + '/.fa-state');\n const file = shellQuote(fdirAbs + '/.fa-state/checkpoints.jsonl');\n const plain = 'mkdir -p ' + dir + \" && printf '%s\\\\n' \" + shellQuote(line) + ' >> ' + file;\n if (typeof line !== 'string' || !line.startsWith('{'))\n return plain;\n return 'ts=$(date -u +%Y-%m-%dT%H:%M:%SZ); mkdir -p ' + dir\n + ' && { printf \\'{\"ts\":\"%s\",\\' \"$ts\"; printf \\'%s\\\\n\\' ' + shellQuote(line.slice(1)) + '; } >> ' + file;\n}\nfunction parseArtifactProbe(opts) {\n if (opts.stdout === null || opts.stdout === undefined)\n return null;\n const known = new Map();\n for (const rel of opts.required)\n known.set('HAVE:' + rel, rel);\n const found = new Set();\n let sentinels = 0;\n for (const raw of String(opts.stdout).split('\\n')) {\n const line = raw.trim();\n if (line === '')\n continue;\n if (line === opts.sentinel) {\n sentinels++;\n continue;\n }\n if (sentinels > 0)\n return null;\n const rel = known.get(line);\n if (rel === undefined)\n return null;\n found.add(rel);\n }\n if (sentinels !== 1)\n return null;\n return found;\n}",
|
|
60
60
|
},
|
|
61
61
|
"training-pairs": {
|
|
62
62
|
name: "training-pairs",
|
package/src/mutation-gate.ts
CHANGED
|
@@ -1488,6 +1488,65 @@ export function summarizeMutationResults(results: readonly MutationEntryResult[]
|
|
|
1488
1488
|
};
|
|
1489
1489
|
}
|
|
1490
1490
|
|
|
1491
|
+
// ── Durable verdicts (instrument-round-b T3/FR-3/A3/A4, ADR-001 D3) ────────────────────────────
|
|
1492
|
+
//
|
|
1493
|
+
// The registry (`test/mutation-registry.json`) is the gate's INPUT — what to mutate. A verdict is
|
|
1494
|
+
// its OUTPUT — what happened the last time the gate ran. Mixing them means the input would change
|
|
1495
|
+
// on every run and every reader of the registry (mutation-registry-freshness included) would have
|
|
1496
|
+
// to start tolerating a moving file; A4 keeps them apart on purpose. `dz mutation-gate` (the CLI
|
|
1497
|
+
// executor) appends one JSON line per classified entry to a SEPARATE durable file
|
|
1498
|
+
// (`.dz/mutation-gate/verdicts.jsonl`) so a verdict survives the executor's own scratch-dir
|
|
1499
|
+
// teardown and the host's next reboot — today it lives only in `DZ_MUTGATE_OUTPUT_DIR`, a temp
|
|
1500
|
+
// directory (00_complexity_assessment.md: 0 of 377 registry entries carry a persisted verdict).
|
|
1501
|
+
|
|
1502
|
+
/** Everything the CLI knows that this pure module does not: when, in which package, and under
|
|
1503
|
+
* which run. `runId` is honest-absent (`null`) when the caller has none to offer — never guessed. */
|
|
1504
|
+
export interface MutationVerdictMeta {
|
|
1505
|
+
readonly ts: string;
|
|
1506
|
+
readonly package: string;
|
|
1507
|
+
readonly runId: string | null;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/** One durable line — append-only, one per classified registry entry. `observed` is the
|
|
1511
|
+
* registry's OWN anchor (copied, never recomputed) so a reader can see the historical coverage
|
|
1512
|
+
* claim next to the fresh verdict without re-opening the registry. */
|
|
1513
|
+
export interface MutationVerdictRow {
|
|
1514
|
+
readonly ts: string;
|
|
1515
|
+
readonly package: string;
|
|
1516
|
+
readonly entryId: string;
|
|
1517
|
+
readonly verdict: MutationVerdict;
|
|
1518
|
+
readonly failingCount: number | null;
|
|
1519
|
+
readonly observed: number | null;
|
|
1520
|
+
readonly drop: boolean;
|
|
1521
|
+
readonly dropComparable: boolean;
|
|
1522
|
+
readonly runId: string | null;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/**
|
|
1526
|
+
* Pure: payload in, row out — no filesystem (NFR-2; the CLI owns the append). `entry` is `null`
|
|
1527
|
+
* for a result that never resolved to a valid registry entry (`ENTRY_INVALID` / `COVERAGE_GAP` —
|
|
1528
|
+
* `parseMutationRegistry` excludes those from `registry.entries` by construction, so the CALLER
|
|
1529
|
+
* cannot always hand one in) — `observed` then stays `null`, honestly, rather than the caller
|
|
1530
|
+
* inventing a fallback entry object just to satisfy this signature.
|
|
1531
|
+
*/
|
|
1532
|
+
export function mutationVerdictRow(
|
|
1533
|
+
entry: MutationRegistryEntry | null,
|
|
1534
|
+
result: MutationEntryResult,
|
|
1535
|
+
meta: MutationVerdictMeta,
|
|
1536
|
+
): MutationVerdictRow {
|
|
1537
|
+
return {
|
|
1538
|
+
ts: meta.ts,
|
|
1539
|
+
package: meta.package,
|
|
1540
|
+
entryId: result.id,
|
|
1541
|
+
verdict: result.verdict,
|
|
1542
|
+
failingCount: result.failingCount,
|
|
1543
|
+
observed: entry !== null && typeof entry.observed === 'number' ? entry.observed : null,
|
|
1544
|
+
drop: result.drop,
|
|
1545
|
+
dropComparable: result.dropComparable,
|
|
1546
|
+
runId: meta.runId,
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1491
1550
|
const VERDICT_MARK: Record<MutationVerdict, string> = {
|
|
1492
1551
|
PROVEN: '✓',
|
|
1493
1552
|
ENTRY_INVALID: '✗',
|
package/src/name-check.ts
CHANGED
|
@@ -127,7 +127,37 @@ export function decideNameCheck(queries: readonly NameQuery[], facts: NameFacts)
|
|
|
127
127
|
return { outcome: 'free', exit: 0, results, reason: `all ${results.length} name(s) are free` };
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Имена того же вида, ДЕЛЯЩИЕ СЛОВО с запрошенным.
|
|
132
|
+
*
|
|
133
|
+
* ЗАЧЕМ ЭТО ЗДЕСЬ. `name-check` отвечает на вопрос «занято ли ИМЯ», и отвечает верно. Но у
|
|
134
|
+
* него есть соседний вопрос, которого он не задаёт: «а нет ли уже ПРИБОРА для этой работы».
|
|
135
|
+
* ИЗМЕРЕНО 2026-09-19: `dz name-check --module ledger-cost-fill` честно ответил FREE, и по
|
|
136
|
+
* этому ответу был построен модуль, дублирующий существующий `ledger-backfill` — подключённый
|
|
137
|
+
* к CLI, с тем же измерением в шапке, заполняющий 47 строк против 31 у дубля и вдобавок
|
|
138
|
+
* намеренно НЕ делающий того, что дубль сделал (вывод минут и агентов, тихо переопределивший
|
|
139
|
+
* две колонки).
|
|
140
|
+
*
|
|
141
|
+
* Поэтому здесь НЕ вердикт и НЕ отказ — свободное имя остаётся свободным. Это СВЕДЕНИЯ в точке
|
|
142
|
+
* решения: список соседей по корню слова, которые оператор увидит ровно тогда, когда ещё не
|
|
143
|
+
* написал ни строки. Разница между «занято» и «рядом есть похожее» сохранена намеренно:
|
|
144
|
+
* превратить второе в отказ значило бы запретить `publish-order` рядом с `publish-signing`.
|
|
145
|
+
*
|
|
146
|
+
* Слова короче четырёх букв игнорируются: `fa`, `dz`, `to`, `run` роднят почти всё со всем.
|
|
147
|
+
*/
|
|
148
|
+
export function nearNames(query: NameQuery, facts: NameFacts, limit = 5): string[] {
|
|
149
|
+
const words = (n: string): string[] =>
|
|
150
|
+
n.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 4);
|
|
151
|
+
const asked = new Set(words(query.name));
|
|
152
|
+
if (asked.size === 0) return [];
|
|
153
|
+
const pool: string[] = query.kind === 'command'
|
|
154
|
+
? [...facts.commands]
|
|
155
|
+
: query.kind === 'module' ? [...facts.modules.keys()] : [...facts.exports.keys()];
|
|
156
|
+
const hits = pool.filter((n) => n !== query.name && words(n).some((w) => asked.has(w)));
|
|
157
|
+
return hits.sort().slice(0, limit);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function renderNameCheck(decision: NameDecision, scanned?: NameFacts['scanned'], facts?: NameFacts): string[] {
|
|
131
161
|
const out: string[] = [];
|
|
132
162
|
if (scanned !== undefined) {
|
|
133
163
|
// Printed always, pass or fail: the operator must be able to see that the sweep looked at a real
|
|
@@ -145,6 +175,18 @@ export function renderNameCheck(decision: NameDecision, scanned?: NameFacts['sca
|
|
|
145
175
|
// Said on the passing path, because that is where the limit gets forgotten: the scan reads
|
|
146
176
|
// declarations, so a re-export under a different name (`export { a as b }`) is invisible to it.
|
|
147
177
|
out.push(' note: this reads declarations in source. A re-export under a different name is not visible here — the build still owns that case.');
|
|
178
|
+
if (facts !== undefined) {
|
|
179
|
+
// Свободное имя — не доказательство отсутствия прибора. Соседи печатаются СВЕДЕНИЯМИ,
|
|
180
|
+
// вердикт при этом не меняется: отказывать за сходство значило бы запретить
|
|
181
|
+
// `publish-order` рядом с `publish-signing`.
|
|
182
|
+
for (const r of decision.results) {
|
|
183
|
+
const near = nearNames({ kind: r.kind, name: r.name }, facts);
|
|
184
|
+
if (near.length > 0) {
|
|
185
|
+
out.push(` рядом уже есть ${r.kind}(s), делящие слово с «${r.name}»: ${near.join(', ')}`);
|
|
186
|
+
out.push(' прочитайте их ПЕРЕД тем, как писать: свободное ИМЯ не означает отсутствия ПРИБОРА для этой работы');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
148
190
|
}
|
|
149
191
|
return out;
|
|
150
192
|
}
|
package/src/no-stubs.ts
CHANGED
|
@@ -118,6 +118,8 @@ export interface StubFinding {
|
|
|
118
118
|
readonly detail: string;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
import { maskMarkdown } from './markdown-masker.js';
|
|
122
|
+
|
|
121
123
|
/** A config waiver: path-keyed, reason MANDATORY. A reasonless entry is refused, never honoured. */
|
|
122
124
|
export interface StubWaiver {
|
|
123
125
|
readonly path?: string;
|
|
@@ -141,18 +143,25 @@ function maskInlineCode(line: string): string {
|
|
|
141
143
|
* clean, while a naked stub line in doc prose still fires. Code files are scanned in full: a marker
|
|
142
144
|
* a code file must legitimately carry (another gate's source, a fixture) takes an inline
|
|
143
145
|
* `no-stubs: <reason>` waiver — visible, reasoned, greppable.
|
|
146
|
+
*
|
|
147
|
+
* Block scoping is DELEGATED to the canonical masker, not re-implemented here. The hand-rolled
|
|
148
|
+
* `inFence = !inFence` toggle it replaces broke two CommonMark rules and produced MEASURED false
|
|
149
|
+
* positives (2026-09-20): a fence closes only on its OWN marker, so a `~~~` line inside a backtick
|
|
150
|
+
* block is content — the toggle read it as a close and scanned the rest of the code block as prose;
|
|
151
|
+
* and a closing fence may carry NO info string, so a second info-string line closed the block.
|
|
152
|
+
* `unclosed: 'hide'` preserves THIS reader's existing policy — an unclosed opener hides the tail
|
|
153
|
+
* rather than restoring it, because a gate must not invent findings out of a half-written block.
|
|
154
|
+
* Four-space indented code stays unmasked (the masker default): unchanged from the toggle era, a
|
|
155
|
+
* known limit rather than a new one.
|
|
144
156
|
*/
|
|
145
157
|
export function scanStubs(path: string, text: unknown): StubFinding[] {
|
|
146
158
|
if (typeof text !== 'string' || text.length === 0) return [];
|
|
147
159
|
const md = isMarkdown(path);
|
|
148
160
|
const out: StubFinding[] = [];
|
|
149
|
-
let inFence = false;
|
|
150
161
|
const lines = text.split('\n');
|
|
162
|
+
const scanLines = md ? String(maskMarkdown(text, { unclosed: 'hide' })).split('\n') : lines;
|
|
151
163
|
for (let i = 0; i < lines.length; i++) {
|
|
152
|
-
const
|
|
153
|
-
if (md && /^\s*(```|~~~)/.test(raw)) { inFence = !inFence; continue; }
|
|
154
|
-
if (md && inFence) continue;
|
|
155
|
-
const line = md ? maskInlineCode(raw) : raw;
|
|
164
|
+
const line = md ? maskInlineCode(scanLines[i] ?? '') : lines[i]!;
|
|
156
165
|
|
|
157
166
|
// Inline waiver first: with a reason ⇒ the line is exempt; without one ⇒ refuse loudly AND
|
|
158
167
|
// leave the marker finding in place (a refused waiver must not half-work).
|
package/src/patterns.ts
CHANGED
|
@@ -755,10 +755,52 @@ export async function reinforcePattern(projectRoot: string, dzIdOrText: string,
|
|
|
755
755
|
return result;
|
|
756
756
|
}
|
|
757
757
|
|
|
758
|
+
/**
|
|
759
|
+
* Which learned pattern does `--reinforce <arg>` mean? PURE, so the branch that REFUSES has a test.
|
|
760
|
+
*
|
|
761
|
+
* Order: the full id, then the exact text, then a BARE id suffix.
|
|
762
|
+
*
|
|
763
|
+
* The suffix step exists because an id is PRINTED as `<namespace>:<suffix>` — `teach:1a4a9bdd…` —
|
|
764
|
+
* by every surface that emits one (`dz teach`'s own `ID:` line, `dz recall --json`'s `dzId`), and a
|
|
765
|
+
* caller who kept only the hex got `no learned pattern matches`. The advisory then pointed at
|
|
766
|
+
* `dz recall`, which cannot surface a QUARANTINED lesson by its own text, so a lesson taught
|
|
767
|
+
* minutes ago had no reachable path at all (MEASURED 2026-09-20, backlog 2d3059fc). The report's
|
|
768
|
+
* wider claim — that ids are rejected outright — is REFUTED: the full printed id has always worked.
|
|
769
|
+
*
|
|
770
|
+
* An ambiguous suffix is refused BY NAME, never guessed. Reinforcing the wrong lesson writes
|
|
771
|
+
* silently and leaves no signal afterwards that the wrong record moved, so a wrong guess here is
|
|
772
|
+
* strictly worse than a refusal the caller can act on. Today only `teach:` ids exist, but `dream:`
|
|
773
|
+
* is a second canonical namespace this store already documents, so the collision is reachable.
|
|
774
|
+
*/
|
|
775
|
+
export function resolveReinforceTarget(
|
|
776
|
+
records: readonly MemoryRecord[],
|
|
777
|
+
dzIdOrText: string,
|
|
778
|
+
): { rec: MemoryRecord; matchedBy: 'id' | 'text' | 'suffix' } | { rec: undefined; error: string } {
|
|
779
|
+
// The KIND of match is part of the answer, not a detail: the caller prints it in a receipt, and a
|
|
780
|
+
// receipt that says "matched by text" about a suffix match is a receipt that lies. MEASURED
|
|
781
|
+
// 2026-09-20: the CLI inferred the kind from `resolved !== input`, which is true for BOTH a text
|
|
782
|
+
// match and a suffix match, so the suffix path reported the wrong one.
|
|
783
|
+
const byId = records.find((r) => r.id === dzIdOrText);
|
|
784
|
+
if (byId !== undefined) return { rec: byId, matchedBy: 'id' };
|
|
785
|
+
const byText = records.find((r) => r.text === dzIdOrText);
|
|
786
|
+
if (byText !== undefined) return { rec: byText, matchedBy: 'text' };
|
|
787
|
+
const bare = records.filter((r) => typeof r.id === 'string' && r.id.endsWith(':' + dzIdOrText));
|
|
788
|
+
if (bare.length > 1) {
|
|
789
|
+
return {
|
|
790
|
+
rec: undefined,
|
|
791
|
+
error: `${JSON.stringify(dzIdOrText)} is a bare id suffix shared by ${bare.length} learned patterns (${bare.map((r) => r.id).join(', ')}) — pass the full id`,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
const only = bare[0];
|
|
795
|
+
if (only !== undefined) return { rec: only, matchedBy: 'suffix' };
|
|
796
|
+
return { rec: undefined, error: `no learned pattern matches ${JSON.stringify(dzIdOrText)}` };
|
|
797
|
+
}
|
|
798
|
+
|
|
758
799
|
async function reinforcePatternLocked(projectRoot: string, dzIdOrText: string, opts: { reward?: number; ts?: string; mergedFrom?: readonly string[]; exposure?: boolean }): Promise<ReinforcePatternResult> {
|
|
759
800
|
const records = loadStoreRecords(projectRoot);
|
|
760
|
-
const
|
|
761
|
-
if (rec === undefined) return { ok: false, error:
|
|
801
|
+
const resolved = resolveReinforceTarget(records, dzIdOrText);
|
|
802
|
+
if (resolved.rec === undefined) return { ok: false, error: resolved.error };
|
|
803
|
+
const rec = resolved.rec;
|
|
762
804
|
const pairId = typeof rec.metadata?.['lessonPairId'] === 'string' ? rec.metadata['lessonPairId'] : undefined;
|
|
763
805
|
const targets = pairId === undefined
|
|
764
806
|
? [rec]
|
|
@@ -781,14 +823,14 @@ async function reinforcePatternLocked(projectRoot: string, dzIdOrText: string, o
|
|
|
781
823
|
for (const target of targets) {
|
|
782
824
|
const promotedMeta = { ...(target.metadata ?? {}), ...encodeReinforcementState(nextState) };
|
|
783
825
|
if (opts.exposure !== true) {
|
|
784
|
-
|
|
785
|
-
|
|
826
|
+
promotedMeta['qStatus'] = 'promoted';
|
|
827
|
+
promotedMeta['promotedAt'] = ts;
|
|
786
828
|
}
|
|
787
829
|
const put = await putStoreRecord(projectRoot, { ...target, metadata: promotedMeta });
|
|
788
830
|
if ('error' in put) return { ok: false, dzId: rec.id, error: put.error };
|
|
789
831
|
}
|
|
790
832
|
try {
|
|
791
|
-
appendFileSync(join(projectRoot, '.dz', 'sessions.jsonl'), JSON.stringify({ event: 'reinforce', ts, dzId: rec.id, uses: nextState.uses }) + '\n');
|
|
833
|
+
appendFileSync(join(projectRoot, '.dz', 'sessions.jsonl'), JSON.stringify({ event: 'reinforce', ts, dzId: rec.id, uses: nextState.uses, exposure: opts.exposure === true }) + '\n');
|
|
792
834
|
} catch { /* best-effort */ }
|
|
793
835
|
return { ok: true, dzId: rec.id, uses: nextState.uses, reward: observed };
|
|
794
836
|
}
|
|
@@ -845,10 +887,9 @@ export async function promotePatterns(projectRoot: string, dzIds: readonly strin
|
|
|
845
887
|
const targets = pairId === undefined ? [rec] : records.filter((r) => r.metadata?.['lessonPairId'] === pairId);
|
|
846
888
|
const pending = targets.filter((target) => !handled.has(target.id) && readQuarantineState(target).quarantined);
|
|
847
889
|
if (pending.length === 0) { notQuarantined.push(id); continue; }
|
|
890
|
+
const promotedAt = new Date().toISOString();
|
|
848
891
|
for (const target of pending) {
|
|
849
|
-
const meta = { ...(target.metadata ?? {}) };
|
|
850
|
-
delete meta['qStatus'];
|
|
851
|
-
delete meta['quarantinedAt'];
|
|
892
|
+
const meta = { ...(target.metadata ?? {}), qStatus: 'promoted', promotedAt };
|
|
852
893
|
const put = await putStoreRecord(projectRoot, { ...target, metadata: meta });
|
|
853
894
|
if ('error' in put) return { ok: false, promoted, notFound, notQuarantined, error: put.error };
|
|
854
895
|
handled.add(target.id);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Что считается ПУБЛИКУЕМЫМ ИСХОДНИКОМ для гейта review-round.
|
|
3
|
+
*
|
|
4
|
+
* ЗАЧЕМ ЭТО ВЫНЕСЕНО. Правило `review-round` — единственный жёсткий гейт, спрашивающий, читал ли
|
|
5
|
+
* изменённый код кто-то, кроме автора. Его сборщик фактов жил одной строкой в `cli.ts` и помечал
|
|
6
|
+
* изменение исходником только по пути `^(src|lib|bin|skills)/`.
|
|
7
|
+
*
|
|
8
|
+
* ИЗМЕРЕНО (запись 5be1bee1, релиз 2026-08-25): пак навыков возит своё содержимое из `templates/`
|
|
9
|
+
* и `<имя-навыка>/`, поэтому `skills-feature-adr` мог поднять версию, изменить сам конвейер
|
|
10
|
+
* `templates/.claude/workflows/feature-adr.js` и гейт `feature-adr/scripts/*.mjs` — и правило НЕ
|
|
11
|
+
* СРАБАТЫВАЛО, а гейт печатал PASS. То есть пакеты, несущие ВЕСЬ конвейер, лежали вне
|
|
12
|
+
* единственной проверки на прочитанность. Страж, который не может сработать, хуже отсутствующего:
|
|
13
|
+
* он создаёт видимость защиты.
|
|
14
|
+
*
|
|
15
|
+
* ПОЧЕМУ НЕ «ВСЁ, ЧТО ПУБЛИКУЕТСЯ». Комментарий прежней строки защищал осознанное решение:
|
|
16
|
+
* «SOURCE = то, что ОТГРУЖАЕТСЯ и может ОШИБИТЬСЯ В РАНТАЙМЕ. Тесты, документация и фикстуры
|
|
17
|
+
* исключены… расширение области — это то, из-за чего жёсткий гейт выключают». Взять список
|
|
18
|
+
* публикуемых файлов целиком значило бы затянуть в гейт всю документацию пака и вернуть ровно ту
|
|
19
|
+
* переусердствующую форму, от которой это решение защищает.
|
|
20
|
+
*
|
|
21
|
+
* ПОЭТОМУ КРИТЕРИЙ — ИСПОЛНИМОСТЬ, А НЕ РАСПОЛОЖЕНИЕ: файл считается исходником, если он либо
|
|
22
|
+
* лежит в одном из исторических каталогов, либо ИСПОЛНЯЕТСЯ у потребителя по своему расширению —
|
|
23
|
+
* где бы он ни лежал. Документация, данные и тесты исключены в обоих случаях.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Каталоги, чьё содержимое отгружается, но исходником для этого гейта не считается. */
|
|
27
|
+
const NOT_SOURCE_DIR = /(^|\/)(test|tests|__tests__|fixtures|__fixtures__|dist|node_modules|coverage)(\/|$)/;
|
|
28
|
+
|
|
29
|
+
/** Исторические каталоги исходников — сохранены дословно, чтобы поведение не поехало. */
|
|
30
|
+
const LEGACY_SOURCE_DIR = /^(src|lib|bin|skills)\//;
|
|
31
|
+
|
|
32
|
+
/** Расширения, которые исполняются у потребителя. Разметка и данные сюда НЕ входят. */
|
|
33
|
+
const EXECUTABLE_EXT = /\.(?:[cm]?js|[cm]?ts|sh|bash|py)$/;
|
|
34
|
+
|
|
35
|
+
/** Расширения, которые исторически исключались даже внутри каталогов исходников. */
|
|
36
|
+
const NEVER_SOURCE_EXT = /\.(?:md|json|txt|yaml|yml)$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Изменение этого файла означает, что пакет отгружает НОВЫЙ ИСПОЛНЯЕМЫЙ код?
|
|
40
|
+
*
|
|
41
|
+
* @param relPath путь ВНУТРИ каталога пакета (`templates/.claude/workflows/feature-adr.js`),
|
|
42
|
+
* а не от корня репозитория
|
|
43
|
+
*/
|
|
44
|
+
export function isShippedSource(relPath: string): boolean {
|
|
45
|
+
if (typeof relPath !== 'string' || relPath === '') return false;
|
|
46
|
+
const p = relPath.replace(/\\/g, '/');
|
|
47
|
+
// Порядок важен: исключение каталога сильнее любого включения, иначе `test/helper.ts`
|
|
48
|
+
// и пересобранный `dist/index.js` начали бы требовать круг ревью.
|
|
49
|
+
if (NOT_SOURCE_DIR.test(p)) return false;
|
|
50
|
+
if (NEVER_SOURCE_EXT.test(p)) return false;
|
|
51
|
+
if (LEGACY_SOURCE_DIR.test(p)) return true;
|
|
52
|
+
return EXECUTABLE_EXT.test(p);
|
|
53
|
+
}
|