@chrisdudek/yg 5.2.0 → 5.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin.js +163 -63
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -337,21 +337,27 @@ it is enabled on types whose changes carry business intent worth capturing.
337
337
  Check the type in \`yg-architecture.yaml\` (or read the node's log state line
338
338
  in \`yg context --node\`) to know whether an entry is required.
339
339
 
340
- A fresh log entry is required BEFORE \`yg check --approve\` whenever BOTH hold:
341
- the node's type has \`log_required: true\` AND the node's mapped source changed
342
- since its last positive closure (the moment all the node's enforced pairs last
343
- went green), or this is the node's first verification and it owns source files.
344
- The entry must be newer than the one recorded at that closure \u2014
345
- one fresh entry per closure cycle. This requirement is INDEPENDENT of aspect
346
- status. A cascade-only re-verification (an aspect edited, the node's source
347
- untouched) needs no new entry.
340
+ A fresh log entry is required whenever BOTH hold: the node's type has
341
+ \`log_required: true\` AND the node's mapped source changed since its last
342
+ positive closure (the moment all the node's enforced pairs last went green), or
343
+ this is the node's first verification and it owns source files. The entry must be
344
+ newer than the one recorded at that closure \u2014 one fresh entry per closure cycle.
345
+ This requirement is a property of the node TYPE plus a source change: it is
346
+ INDEPENDENT of aspect status AND of whether the node has any aspects or pairs at
347
+ all. A node that owns source but has no effective (non-draft) aspects still needs
348
+ an entry when its source changes. A cascade-only re-verification (an aspect
349
+ edited, the node's source untouched) needs no new entry.
348
350
 
349
351
  1. Edit source files
350
352
  2. \`yg log add --node <path> --reason "<justification>"\`
351
353
  3. \`yg check --approve\`
352
354
 
353
- If you forget step 2: the gate raises a clear error and that node's pairs are
354
- skipped (other nodes proceed); add the entry and re-run.
355
+ The requirement is enforced READ-ONLY: a missing entry is a blocking \`yg check\`
356
+ error in its own right, not only a \`--approve\`-time gate \u2014 so plain \`yg check\`
357
+ (and a CI run on it) catches an unlogged source change even on a node that
358
+ produces no pairs. If you forget step 2: \`yg check\` flags that node; at
359
+ \`--approve\` a node that has pairs has them skipped (other nodes proceed) and the
360
+ run stays red until the entry exists. Add the entry and re-run.
355
361
 
356
362
  If a pair is refused, iterate on the code WITHOUT adding new log entries. One
357
363
  log entry covers all source edits until the node reaches positive closure \u2014
@@ -3294,7 +3300,7 @@ ${msg.next}`;
3294
3300
  }
3295
3301
 
3296
3302
  // src/io/lock-store.ts
3297
- import { readFileSync } from "fs";
3303
+ import { readFileSync, unlinkSync } from "fs";
3298
3304
  import path11 from "path";
3299
3305
 
3300
3306
  // src/model/lock.ts
@@ -3589,11 +3595,24 @@ async function writeFileIfChanged(filePath, content20) {
3589
3595
  }
3590
3596
  await atomicWriteFile(filePath, content20);
3591
3597
  }
3598
+ function removeFileIfExists(filePath) {
3599
+ try {
3600
+ unlinkSync(filePath);
3601
+ } catch (e) {
3602
+ if (e.code !== "ENOENT") throw e;
3603
+ }
3604
+ }
3605
+ async function writeOrRemoveSplitFile(filePath, version, verdicts, nodes) {
3606
+ if (Object.keys(verdicts).length === 0 && Object.keys(nodes).length === 0) {
3607
+ removeFileIfExists(filePath);
3608
+ return;
3609
+ }
3610
+ await writeFileIfChanged(filePath, serializeLock({ version, verdicts, nodes }));
3611
+ }
3592
3612
  async function writeLock(yggRoot, lock, opts = {}) {
3593
3613
  const scope = opts.scope ?? "all";
3594
3614
  if (scope === "logs") {
3595
- const content20 = serializeLock({ version: lock.version, verdicts: {}, nodes: lock.nodes });
3596
- await writeFileIfChanged(logsLockPath(yggRoot), content20);
3615
+ await writeOrRemoveSplitFile(logsLockPath(yggRoot), lock.version, {}, lock.nodes);
3597
3616
  return;
3598
3617
  }
3599
3618
  const detIds = opts.deterministicAspectIds;
@@ -3607,7 +3626,7 @@ async function writeLock(yggRoot, lock, opts = {}) {
3607
3626
  return;
3608
3627
  }
3609
3628
  await writeFileIfChanged(nondetLockPath(yggRoot), serializeLock({ version: lock.version, verdicts: nondet, nodes: {} }));
3610
- await writeFileIfChanged(logsLockPath(yggRoot), serializeLock({ version: lock.version, verdicts: {}, nodes: lock.nodes }));
3629
+ await writeOrRemoveSplitFile(logsLockPath(yggRoot), lock.version, {}, lock.nodes);
3611
3630
  await writeFileIfChanged(detLockPath(yggRoot), serializeLock({ version: lock.version, verdicts: det, nodes: {} }));
3612
3631
  }
3613
3632
 
@@ -7868,12 +7887,33 @@ function parseLog(content20) {
7868
7887
  }
7869
7888
 
7870
7889
  // src/core/log/log-gate.ts
7890
+ async function logGateBlocksNode(graph, projectRoot, node, lock) {
7891
+ const archType = graph.architecture.node_types[node.meta.type];
7892
+ const logRequired = archType?.log_required ?? false;
7893
+ if (!logRequired) return false;
7894
+ let currentFingerprint;
7895
+ try {
7896
+ currentFingerprint = await computeSourceFingerprint(graph, node.path);
7897
+ } catch (e) {
7898
+ if (e instanceof FileUnreadableError) {
7899
+ debugWrite(`[log-gate] logGate fingerprint for ${toPosixPath(node.path)}: ${e.message}`);
7900
+ return true;
7901
+ }
7902
+ throw e;
7903
+ }
7904
+ if (currentFingerprint === void 0) return false;
7905
+ const storedFingerprint = lock.nodes[node.path]?.source;
7906
+ const drifted = currentFingerprint !== storedFingerprint;
7907
+ if (!drifted) return false;
7908
+ const logContent = await readLogContent(projectRoot, node.path);
7909
+ return !hasFreshLogEntry(logContent, lock.nodes[node.path]?.log);
7910
+ }
7871
7911
  async function readLogContent(projectRoot, nodePath) {
7872
7912
  const logAbs = path22.join(projectRoot, ".yggdrasil", "model", nodePath, "log.md");
7873
7913
  try {
7874
7914
  return await readTextFile(logAbs);
7875
7915
  } catch (e) {
7876
- debugWrite(`[log-gate] readLogContent: could not read ${logAbs}: ${e instanceof Error ? e.message : String(e)}`);
7916
+ debugWrite(`[log-gate] readLogContent: could not read ${toPosixPath(logAbs)}: ${e instanceof Error ? e.message : String(e)}`);
7877
7917
  return "";
7878
7918
  }
7879
7919
  }
@@ -14336,6 +14376,23 @@ ${violations.map((v) => ` line ${v.line}: ${v.reason} \u2014 ${v.detail}`).join
14336
14376
  }
14337
14377
  }
14338
14378
  }
14379
+ async function classifyLogRequirement(graph, projectRoot, lock, unreadableNodes, issues) {
14380
+ for (const [nodePath, node] of graph.nodes) {
14381
+ if (unreadableNodes.has(nodePath)) continue;
14382
+ if (!await logGateBlocksNode(graph, projectRoot, node, lock)) continue;
14383
+ issues.push({
14384
+ severity: "error",
14385
+ code: "log-entry-missing",
14386
+ rule: "log-entry-missing",
14387
+ messageData: {
14388
+ what: `No fresh log entry for node '${toPosixPath(nodePath)}' \u2014 its source changed but no justification entry exists.`,
14389
+ why: `Node type '${node.meta.type}' has log_required: true \u2014 every source change needs a log entry capturing WHY. The requirement is a property of the node type plus a source change, independent of aspects; yg check stays red until a fresh entry exists.`,
14390
+ next: `yg log add --node ${toPosixPath(nodePath)} --reason '<justification>', then re-run: yg check --approve`
14391
+ },
14392
+ nodePath
14393
+ });
14394
+ }
14395
+ }
14339
14396
  function scanUncoveredFiles(graph, gitTrackedFiles) {
14340
14397
  const allMappings = [];
14341
14398
  for (const node of graph.nodes.values()) {
@@ -14443,6 +14500,8 @@ async function runCheck(graph, gitTrackedFiles) {
14443
14500
  });
14444
14501
  }
14445
14502
  await classifyLogStateFromLock(graph, projectRoot, lock, lockIssues);
14503
+ const unreadableNodes = new Set(verification.unreadable.map((u) => u.nodePath));
14504
+ await classifyLogRequirement(graph, projectRoot, lock, unreadableNodes, lockIssues);
14446
14505
  } catch (err) {
14447
14506
  if (err instanceof LockInvalidError) {
14448
14507
  lockIssues.push({
@@ -14521,6 +14580,8 @@ function computeSuggestedNext(issues) {
14521
14580
  }
14522
14581
  const lockInvalid = errors.find((i) => i.code === "lock-invalid");
14523
14582
  if (lockInvalid) return lockInvalid.messageData.next;
14583
+ const logEntryMissing = errors.find((i) => i.code === "log-entry-missing");
14584
+ if (logEntryMissing) return logEntryMissing.messageData.next;
14524
14585
  const unverified = errors.find((i) => i.code === "unverified");
14525
14586
  if (unverified) return unverified.messageData.next;
14526
14587
  const enforcedRefusal = errors.find((i) => i.code === "aspect-violation-enforced");
@@ -14678,7 +14739,10 @@ function extractLastVerdict(text2) {
14678
14739
  }
14679
14740
  function parseAspectResponse(output) {
14680
14741
  const trimmed = output.trim();
14681
- if (!trimmed) return void 0;
14742
+ if (!trimmed) {
14743
+ debugWrite("[parseAspectResponse] reviewer returned an empty reply \u2014 nothing to parse");
14744
+ return void 0;
14745
+ }
14682
14746
  try {
14683
14747
  return normalizeResponse(JSON.parse(trimmed));
14684
14748
  } catch (err) {
@@ -14696,7 +14760,7 @@ function parseAspectResponse(output) {
14696
14760
  if (verdict) return normalizeResponse(verdict);
14697
14761
  const salvaged = salvageVerdict(trimmed);
14698
14762
  if (salvaged) return salvaged;
14699
- debugWrite("[parseAspectResponse] no parseable JSON verdict \u2014 classifying as provider error");
14763
+ debugWrite(`[parseAspectResponse] no parseable JSON verdict \u2014 classifying as provider error. Raw reply (${output.length} chars): ${output}`);
14700
14764
  return { satisfied: false, reason: `Unparseable reviewer response: ${trimmed.slice(0, 160)}`, errorSource: "provider" };
14701
14765
  }
14702
14766
  var CliAgentProvider = class {
@@ -15340,27 +15404,6 @@ async function runPairPool(items, concurrency, fn) {
15340
15404
  }
15341
15405
 
15342
15406
  // src/core/fill-log-gate.ts
15343
- async function logGateBlocksNode(graph, projectRoot, node, lock) {
15344
- const archType = graph.architecture.node_types[node.meta.type];
15345
- const logRequired = archType?.log_required ?? false;
15346
- if (!logRequired) return false;
15347
- let currentFingerprint;
15348
- try {
15349
- currentFingerprint = await computeSourceFingerprint(graph, node.path);
15350
- } catch (e) {
15351
- if (e instanceof FileUnreadableError) {
15352
- debugWrite(`[fill] logGate fingerprint for ${toPosixPath(node.path)}: ${e.message}`);
15353
- return true;
15354
- }
15355
- throw e;
15356
- }
15357
- if (currentFingerprint === void 0) return false;
15358
- const storedFingerprint = lock.nodes[node.path]?.source;
15359
- const drifted = currentFingerprint !== storedFingerprint;
15360
- if (!drifted) return false;
15361
- const logContent = await readLogContent(projectRoot, node.path);
15362
- return !hasFreshLogEntry(logContent, lock.nodes[node.path]?.log);
15363
- }
15364
15407
  async function logGateBlocks(graph, projectRoot, node, lock, emitIssue) {
15365
15408
  const blocked = await logGateBlocksNode(graph, projectRoot, node, lock);
15366
15409
  if (!blocked) return false;
@@ -15383,6 +15426,12 @@ async function applyPositiveClosure(graph, projectRoot, lock, blockedNodes, pers
15383
15426
  }
15384
15427
  let mutated = false;
15385
15428
  for (const [nodePath, node] of graph.nodes) {
15429
+ const archType = graph.architecture.node_types[node.meta.type];
15430
+ const logRequired = archType?.log_required ?? false;
15431
+ if (!logRequired) {
15432
+ if (await reconcileNonLogRequiredEntry(projectRoot, nodePath, lock)) mutated = true;
15433
+ continue;
15434
+ }
15386
15435
  let currentFingerprint;
15387
15436
  try {
15388
15437
  currentFingerprint = await computeSourceFingerprint(graph, nodePath);
@@ -15394,8 +15443,7 @@ async function applyPositiveClosure(graph, projectRoot, lock, blockedNodes, pers
15394
15443
  throw e;
15395
15444
  }
15396
15445
  if (currentFingerprint === void 0) {
15397
- await closeLogBaselineOnly(projectRoot, nodePath, lock);
15398
- mutated = true;
15446
+ if (await closeLogBaselineOnly(projectRoot, nodePath, lock)) mutated = true;
15399
15447
  continue;
15400
15448
  }
15401
15449
  const stored = lock.nodes[nodePath]?.source;
@@ -15414,12 +15462,32 @@ async function applyPositiveClosure(graph, projectRoot, lock, blockedNodes, pers
15414
15462
  }
15415
15463
  if (mutated) await persistLock();
15416
15464
  }
15465
+ function logBaselineEquals(a, b) {
15466
+ if (!a || !b) return !a && !b;
15467
+ return a.last_entry_datetime === b.last_entry_datetime && a.prefix_hash === b.prefix_hash;
15468
+ }
15469
+ async function reconcileNonLogRequiredEntry(projectRoot, nodePath, lock) {
15470
+ const logBaseline = await computeLogBaselineForNode(projectRoot, nodePath);
15471
+ const existing = lock.nodes[nodePath];
15472
+ const desiredLog = logBaseline ?? existing?.log;
15473
+ if (!desiredLog) {
15474
+ if (existing === void 0) return false;
15475
+ delete lock.nodes[nodePath];
15476
+ return true;
15477
+ }
15478
+ if (existing !== void 0 && existing.source === void 0 && logBaselineEquals(existing.log, desiredLog)) {
15479
+ return false;
15480
+ }
15481
+ lock.nodes[nodePath] = { log: desiredLog };
15482
+ return true;
15483
+ }
15417
15484
  async function closeLogBaselineOnly(projectRoot, nodePath, lock) {
15418
15485
  const logBaseline = await computeLogBaselineForNode(projectRoot, nodePath);
15419
- if (!logBaseline) return;
15420
- const entry = lock.nodes[nodePath] ?? {};
15421
- entry.log = logBaseline;
15422
- lock.nodes[nodePath] = entry;
15486
+ if (!logBaseline) return false;
15487
+ const existing = lock.nodes[nodePath];
15488
+ if (logBaselineEquals(existing?.log, logBaseline)) return false;
15489
+ lock.nodes[nodePath] = { ...existing ?? {}, log: logBaseline };
15490
+ return true;
15423
15491
  }
15424
15492
 
15425
15493
  // src/core/fill-gc.ts
@@ -15882,6 +15950,7 @@ function formatOutput(result, view = { kind: "full" }) {
15882
15950
  }
15883
15951
  var ERROR_CODE_PRIORITY = [
15884
15952
  "lock-invalid",
15953
+ "log-entry-missing",
15885
15954
  "unverified",
15886
15955
  "aspect-violation-enforced",
15887
15956
  "prompt-too-large",
@@ -15996,7 +16065,7 @@ function renderErrorSection(errors) {
15996
16065
  const architecture = errors.filter((i) => ARCHITECTURE_CODES.has(i.code));
15997
16066
  const completeness = errors.filter((i) => COMPLETENESS_CODES.has(i.code));
15998
16067
  const strict = errors.filter((i) => STRICT_CODES.has(i.code));
15999
- const logErrors = errors.filter((i) => i.code === "log-conflict" || i.code === "log-integrity" || i.code === "log-format");
16068
+ const logErrors = errors.filter((i) => i.code === "log-conflict" || i.code === "log-integrity" || i.code === "log-format" || i.code === "log-entry-missing");
16000
16069
  const aspectErrors = errors.filter((i) => i.code === "aspect-violation-enforced");
16001
16070
  const remaining = errors.filter(
16002
16071
  (i) => !verification.includes(i) && !unmapped.includes(i) && !structural.includes(i) && !architecture.includes(i) && !completeness.includes(i) && !strict.includes(i) && !logErrors.includes(i) && !aspectErrors.includes(i)
@@ -17759,9 +17828,12 @@ prefix (e.g. \`src/handlers\`) covers that file or all files beneath it.
17759
17828
  ## log_required \u2014 when to enable the log gate
17760
17829
 
17761
17830
  Each node type may set \`log_required\` (default \`false\`). When \`true\`, a node of
17762
- that type demands a fresh log entry before \`yg check --approve\` whenever its
17763
- mapped source changed since the node's last positive closure (the log gate, see
17764
- \`yg knowledge read log-management\`).
17831
+ that type demands a fresh log entry whenever its mapped source changed since the
17832
+ node's last positive closure. The requirement is a property of the node TYPE plus
17833
+ a source change \u2014 independent of the node's aspects or pairs \u2014 and is enforced
17834
+ read-only: plain \`yg check\` flags a missing entry as a blocking error, so it
17835
+ bites even on a node that produces no pairs (the log gate, see \`yg knowledge read
17836
+ log-management\`).
17765
17837
 
17766
17838
  Enable it on types whose changes carry business intent worth capturing \u2014
17767
17839
  domain logic, command handlers, persistence adapters, anything where the WHY
@@ -19437,8 +19509,8 @@ pairs on that node \u2014 a legitimate vacuous pass, no verdict, no entry.
19437
19509
  }
19438
19510
  },
19439
19511
  "nodes": {
19440
- "billing/cancel": {
19441
- "source": "<sha256>", // source fingerprint \u2014 the log gate's basis
19512
+ "billing/cancel": { // present ONLY when log_required and/or a log.md exists
19513
+ "source": "<sha256>", // source fingerprint \u2014 log gate basis; ONLY for log_required nodes
19442
19514
  "log": { "last_entry_datetime": "<ISO>", "prefix_hash": "<sha256>" }
19443
19515
  }
19444
19516
  }
@@ -19460,6 +19532,12 @@ pairs on that node \u2014 a legitimate vacuous pass, no verdict, no entry.
19460
19532
  entirely.
19461
19533
  - \`nodes.<path>\` carries the source fingerprint (the log gate's contract,
19462
19534
  \`yg knowledge read log-management\`) and the append-only log integrity baseline.
19535
+ The source fingerprint is the log gate's drift basis, so it is recorded ONLY for
19536
+ \`log_required\` nodes \u2014 a non-log_required node gets a \`nodes\` entry only when it
19537
+ owns a \`log.md\` (then holding just the \`log\` baseline, no \`source\`). When the
19538
+ \`nodes\` section is empty (no log_required node, no \`log.md\`), \`yg-lock.logs.json\`
19539
+ is not written at all \u2014 an empty committed husk is removed, and readLock treats
19540
+ an absent file as empty state.
19463
19541
  - The built-in relation-conformance check is NOT stored in the lock \u2014 it is
19464
19542
  recomputed live on every \`yg check\`. The lock holds only aspect \`verdicts\`
19465
19543
  and per-node \`nodes\` facts; there is no relation section. See "Relation
@@ -20071,9 +20149,12 @@ real \`--approve\` bills at most that many calls. The preview always exits 0, ev
20071
20149
  when enforced pairs are unverified; it never blocks. Only a broken
20072
20150
  configuration (the step-1 structural gate) aborts the preview \u2014 it surfaces the
20073
20151
  same blocker a real \`--approve\` would hit. A cost estimate never demands a fresh
20074
- log entry, so the preview also bypasses the per-node log gate \u2014 it previews even
20075
- on \`log_required\` nodes whose source changed since their last closure, where the
20076
- real \`--approve\` would require the log entry first. \`--dry-run\` requires
20152
+ log entry, so the preview never HARD-STOPS on the per-node log gate \u2014 it previews
20153
+ the budget even on \`log_required\` nodes whose source changed since their last
20154
+ closure, where a real \`--approve\` would require the entry first. (The preview's
20155
+ trailing read-only check report still SURFACES that requirement as a
20156
+ \`log-entry-missing\` error, exactly as plain \`yg check\` does \u2014 it just exits 0
20157
+ and writes nothing.) \`--dry-run\` requires
20077
20158
  \`--approve\`; on its own it is a usage error (plain \`yg check\` is already a free,
20078
20159
  no-write read).
20079
20160
 
@@ -20316,7 +20397,7 @@ working-with-architecture\` is the home for that guidance). To know whether a no
20316
20397
  needs an entry, check its type in \`yg-architecture.yaml\`, or read the node's log
20317
20398
  state line in \`yg context --node\`.
20318
20399
 
20319
- A fresh log entry is required BEFORE \`yg check --approve\` whenever BOTH hold:
20400
+ A fresh log entry is required whenever BOTH hold:
20320
20401
 
20321
20402
  - the node's type has \`log_required: true\`, AND
20322
20403
  - the node's mapped source changed since its last positive closure (or this is the
@@ -20324,10 +20405,21 @@ A fresh log entry is required BEFORE \`yg check --approve\` whenever BOTH hold:
20324
20405
 
20325
20406
  "Fresh" means newer than the entry recorded at that closure \u2014 one fresh entry per
20326
20407
  closure cycle. The requirement depends ONLY on the type flag and the source
20327
- change. It is INDEPENDENT of aspect status. A node with no source change
20408
+ change. It is INDEPENDENT of aspect status AND of whether the node has any aspects
20409
+ or pairs at all \u2014 a node that owns source but has no effective (non-draft) aspects
20410
+ still needs an entry when its source changes. A node with no source change
20328
20411
  (cascade-only re-verification \u2014 an aspect was edited, the source untouched) needs
20329
20412
  no new entry.
20330
20413
 
20414
+ The requirement is enforced READ-ONLY. A missing entry is a blocking
20415
+ \`log-entry-missing\` error surfaced by plain \`yg check\` itself \u2014 computed live
20416
+ from each node's source fingerprint, like the relation-conformance check, at zero
20417
+ LLM cost \u2014 not merely a \`--approve\`-time gate. So a CI run on plain \`yg check\`
20418
+ catches an unlogged source change on a \`log_required\` node even when that node
20419
+ produces no pairs to fill. \`--approve\` only WRITES (records the closure baseline
20420
+ once the entry exists); it never gets a chance to bypass the requirement, because
20421
+ its own final re-check surfaces the same error.
20422
+
20331
20423
  ## Positive closure \u2014 the cycle
20332
20424
 
20333
20425
  Positive closure is the moment a \`yg check --approve\` run ends with every ENFORCED
@@ -20337,7 +20429,9 @@ records the node's source fingerprint and the log freshness baseline.
20337
20429
  Corollaries:
20338
20430
  - Advisory refusals do NOT prevent closure.
20339
20431
  - A node with only advisory/deterministic aspects, or no pairs at all, closes
20340
- vacuously.
20432
+ vacuously \u2014 BUT only once the log requirement is satisfied: a \`log_required\`
20433
+ node whose source changed with no fresh entry does not close, and \`yg check\`
20434
+ flags it red regardless of its (lack of) pairs.
20341
20435
  - A red enforced pair of either kind keeps the cycle OPEN \u2014 the same log entry
20342
20436
  stays valid through every retry until the node is actually green. Intent does
20343
20437
  not change between retries; only execution does.
@@ -20348,9 +20442,13 @@ The gate's "mapped source changed" test is computed from a per-node **source
20348
20442
  fingerprint** \u2014 one sha256 fold over the sorted \`[path, sha256(bytes)]\` list of
20349
20443
  ALL the node's mapped files (the full mapping, not the scope-filtered subject
20350
20444
  sets; binaries included by bytes). It lives in \`yg-lock.logs.json\` under
20351
- \`nodes.<path>.source\`, written at positive closure. The append-only log integrity
20352
- baseline (boundary datetime + prefix hash) lives beside it under
20353
- \`nodes.<path>.log\`. There is no separate per-node state file.
20445
+ \`nodes.<path>.source\`, written at positive closure \u2014 but ONLY for \`log_required\`
20446
+ nodes, since the fingerprint is the gate's drift basis and the gate never runs
20447
+ elsewhere. A non-log_required node records no \`source\`; it gets a \`nodes\` entry
20448
+ only when it owns a \`log.md\`, holding just the append-only \`log\` baseline
20449
+ (boundary datetime + prefix hash). When the section is empty (no log_required
20450
+ node, no \`log.md\`), \`yg-lock.logs.json\` is not written at all \u2014 an empty committed
20451
+ husk is removed. There is no separate per-node state file.
20354
20452
 
20355
20453
  The basic workflow:
20356
20454
 
@@ -20358,10 +20456,12 @@ The basic workflow:
20358
20456
  2. \`yg log add --node <path> --reason "<justification>"\`
20359
20457
  3. \`yg check --approve\`
20360
20458
 
20361
- If you forget step 2, the gate raises \`log-entry-missing\` and that node's pairs
20362
- are skipped (other nodes proceed); add the entry and re-run. If a pair is refused,
20363
- iterate on the code WITHOUT adding new log entries \u2014 one entry covers all edits
20364
- until the node reaches closure.
20459
+ If you forget step 2, plain \`yg check\` raises a blocking \`log-entry-missing\`
20460
+ error for that node (caught read-only, regardless of whether the node has pairs);
20461
+ at \`--approve\` a node that has pairs additionally has them skipped while other
20462
+ nodes proceed, and the run stays red until the entry exists. Add the entry and
20463
+ re-run. If a pair is refused, iterate on the code WITHOUT adding new log entries \u2014
20464
+ one entry covers all edits until the node reaches closure.
20365
20465
 
20366
20466
  ## Self-contained entry \u2014 worked example
20367
20467
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrisdudek/yg",
3
- "version": "5.2.0",
3
+ "version": "5.2.2",
4
4
  "description": "Architecture rules your AI coding agent can't ignore. It gets the rules for a file before it edits, and every change is checked — by a free local script or an LLM reviewer — before it moves on. Works with Claude Code, Cursor, Copilot, Codex, Cline.",
5
5
  "type": "module",
6
6
  "bin": {