@chrisdudek/yg 5.2.1 → 5.2.3

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 +165 -65
  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;
@@ -3602,13 +3621,12 @@ async function writeLock(yggRoot, lock, opts = {}) {
3602
3621
  }
3603
3622
  const { det, nondet } = partitionVerdicts(lock.verdicts, detIds);
3604
3623
  if (scope === "deterministic") {
3605
- const content20 = serializeLock({ version: lock.version, verdicts: det, nodes: {} });
3606
- await writeFileIfChanged(detLockPath(yggRoot), content20);
3624
+ await writeOrRemoveSplitFile(detLockPath(yggRoot), lock.version, det, {});
3607
3625
  return;
3608
3626
  }
3609
- await writeFileIfChanged(nondetLockPath(yggRoot), serializeLock({ version: lock.version, verdicts: nondet, nodes: {} }));
3610
- await writeFileIfChanged(logsLockPath(yggRoot), serializeLock({ version: lock.version, verdicts: {}, nodes: lock.nodes }));
3611
- await writeFileIfChanged(detLockPath(yggRoot), serializeLock({ version: lock.version, verdicts: det, nodes: {} }));
3627
+ await writeOrRemoveSplitFile(nondetLockPath(yggRoot), lock.version, nondet, {});
3628
+ await writeOrRemoveSplitFile(logsLockPath(yggRoot), lock.version, {}, lock.nodes);
3629
+ await writeOrRemoveSplitFile(detLockPath(yggRoot), lock.version, det, {});
3612
3630
  }
3613
3631
 
3614
3632
  // src/cli/preamble.ts
@@ -7868,12 +7886,33 @@ function parseLog(content20) {
7868
7886
  }
7869
7887
 
7870
7888
  // src/core/log/log-gate.ts
7889
+ async function logGateBlocksNode(graph, projectRoot, node, lock) {
7890
+ const archType = graph.architecture.node_types[node.meta.type];
7891
+ const logRequired = archType?.log_required ?? false;
7892
+ if (!logRequired) return false;
7893
+ let currentFingerprint;
7894
+ try {
7895
+ currentFingerprint = await computeSourceFingerprint(graph, node.path);
7896
+ } catch (e) {
7897
+ if (e instanceof FileUnreadableError) {
7898
+ debugWrite(`[log-gate] logGate fingerprint for ${toPosixPath(node.path)}: ${e.message}`);
7899
+ return true;
7900
+ }
7901
+ throw e;
7902
+ }
7903
+ if (currentFingerprint === void 0) return false;
7904
+ const storedFingerprint = lock.nodes[node.path]?.source;
7905
+ const drifted = currentFingerprint !== storedFingerprint;
7906
+ if (!drifted) return false;
7907
+ const logContent = await readLogContent(projectRoot, node.path);
7908
+ return !hasFreshLogEntry(logContent, lock.nodes[node.path]?.log);
7909
+ }
7871
7910
  async function readLogContent(projectRoot, nodePath) {
7872
7911
  const logAbs = path22.join(projectRoot, ".yggdrasil", "model", nodePath, "log.md");
7873
7912
  try {
7874
7913
  return await readTextFile(logAbs);
7875
7914
  } catch (e) {
7876
- debugWrite(`[log-gate] readLogContent: could not read ${logAbs}: ${e instanceof Error ? e.message : String(e)}`);
7915
+ debugWrite(`[log-gate] readLogContent: could not read ${toPosixPath(logAbs)}: ${e instanceof Error ? e.message : String(e)}`);
7877
7916
  return "";
7878
7917
  }
7879
7918
  }
@@ -14336,6 +14375,23 @@ ${violations.map((v) => ` line ${v.line}: ${v.reason} \u2014 ${v.detail}`).join
14336
14375
  }
14337
14376
  }
14338
14377
  }
14378
+ async function classifyLogRequirement(graph, projectRoot, lock, unreadableNodes, issues) {
14379
+ for (const [nodePath, node] of graph.nodes) {
14380
+ if (unreadableNodes.has(nodePath)) continue;
14381
+ if (!await logGateBlocksNode(graph, projectRoot, node, lock)) continue;
14382
+ issues.push({
14383
+ severity: "error",
14384
+ code: "log-entry-missing",
14385
+ rule: "log-entry-missing",
14386
+ messageData: {
14387
+ what: `No fresh log entry for node '${toPosixPath(nodePath)}' \u2014 its source changed but no justification entry exists.`,
14388
+ 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.`,
14389
+ next: `yg log add --node ${toPosixPath(nodePath)} --reason '<justification>', then re-run: yg check --approve`
14390
+ },
14391
+ nodePath
14392
+ });
14393
+ }
14394
+ }
14339
14395
  function scanUncoveredFiles(graph, gitTrackedFiles) {
14340
14396
  const allMappings = [];
14341
14397
  for (const node of graph.nodes.values()) {
@@ -14443,6 +14499,8 @@ async function runCheck(graph, gitTrackedFiles) {
14443
14499
  });
14444
14500
  }
14445
14501
  await classifyLogStateFromLock(graph, projectRoot, lock, lockIssues);
14502
+ const unreadableNodes = new Set(verification.unreadable.map((u) => u.nodePath));
14503
+ await classifyLogRequirement(graph, projectRoot, lock, unreadableNodes, lockIssues);
14446
14504
  } catch (err) {
14447
14505
  if (err instanceof LockInvalidError) {
14448
14506
  lockIssues.push({
@@ -14521,6 +14579,8 @@ function computeSuggestedNext(issues) {
14521
14579
  }
14522
14580
  const lockInvalid = errors.find((i) => i.code === "lock-invalid");
14523
14581
  if (lockInvalid) return lockInvalid.messageData.next;
14582
+ const logEntryMissing = errors.find((i) => i.code === "log-entry-missing");
14583
+ if (logEntryMissing) return logEntryMissing.messageData.next;
14524
14584
  const unverified = errors.find((i) => i.code === "unverified");
14525
14585
  if (unverified) return unverified.messageData.next;
14526
14586
  const enforcedRefusal = errors.find((i) => i.code === "aspect-violation-enforced");
@@ -15343,27 +15403,6 @@ async function runPairPool(items, concurrency, fn) {
15343
15403
  }
15344
15404
 
15345
15405
  // src/core/fill-log-gate.ts
15346
- async function logGateBlocksNode(graph, projectRoot, node, lock) {
15347
- const archType = graph.architecture.node_types[node.meta.type];
15348
- const logRequired = archType?.log_required ?? false;
15349
- if (!logRequired) return false;
15350
- let currentFingerprint;
15351
- try {
15352
- currentFingerprint = await computeSourceFingerprint(graph, node.path);
15353
- } catch (e) {
15354
- if (e instanceof FileUnreadableError) {
15355
- debugWrite(`[fill] logGate fingerprint for ${toPosixPath(node.path)}: ${e.message}`);
15356
- return true;
15357
- }
15358
- throw e;
15359
- }
15360
- if (currentFingerprint === void 0) return false;
15361
- const storedFingerprint = lock.nodes[node.path]?.source;
15362
- const drifted = currentFingerprint !== storedFingerprint;
15363
- if (!drifted) return false;
15364
- const logContent = await readLogContent(projectRoot, node.path);
15365
- return !hasFreshLogEntry(logContent, lock.nodes[node.path]?.log);
15366
- }
15367
15406
  async function logGateBlocks(graph, projectRoot, node, lock, emitIssue) {
15368
15407
  const blocked = await logGateBlocksNode(graph, projectRoot, node, lock);
15369
15408
  if (!blocked) return false;
@@ -15386,6 +15425,12 @@ async function applyPositiveClosure(graph, projectRoot, lock, blockedNodes, pers
15386
15425
  }
15387
15426
  let mutated = false;
15388
15427
  for (const [nodePath, node] of graph.nodes) {
15428
+ const archType = graph.architecture.node_types[node.meta.type];
15429
+ const logRequired = archType?.log_required ?? false;
15430
+ if (!logRequired) {
15431
+ if (await reconcileNonLogRequiredEntry(projectRoot, nodePath, lock)) mutated = true;
15432
+ continue;
15433
+ }
15389
15434
  let currentFingerprint;
15390
15435
  try {
15391
15436
  currentFingerprint = await computeSourceFingerprint(graph, nodePath);
@@ -15397,8 +15442,7 @@ async function applyPositiveClosure(graph, projectRoot, lock, blockedNodes, pers
15397
15442
  throw e;
15398
15443
  }
15399
15444
  if (currentFingerprint === void 0) {
15400
- await closeLogBaselineOnly(projectRoot, nodePath, lock);
15401
- mutated = true;
15445
+ if (await closeLogBaselineOnly(projectRoot, nodePath, lock)) mutated = true;
15402
15446
  continue;
15403
15447
  }
15404
15448
  const stored = lock.nodes[nodePath]?.source;
@@ -15417,12 +15461,32 @@ async function applyPositiveClosure(graph, projectRoot, lock, blockedNodes, pers
15417
15461
  }
15418
15462
  if (mutated) await persistLock();
15419
15463
  }
15464
+ function logBaselineEquals(a, b) {
15465
+ if (!a || !b) return !a && !b;
15466
+ return a.last_entry_datetime === b.last_entry_datetime && a.prefix_hash === b.prefix_hash;
15467
+ }
15468
+ async function reconcileNonLogRequiredEntry(projectRoot, nodePath, lock) {
15469
+ const logBaseline = await computeLogBaselineForNode(projectRoot, nodePath);
15470
+ const existing = lock.nodes[nodePath];
15471
+ const desiredLog = logBaseline ?? existing?.log;
15472
+ if (!desiredLog) {
15473
+ if (existing === void 0) return false;
15474
+ delete lock.nodes[nodePath];
15475
+ return true;
15476
+ }
15477
+ if (existing !== void 0 && existing.source === void 0 && logBaselineEquals(existing.log, desiredLog)) {
15478
+ return false;
15479
+ }
15480
+ lock.nodes[nodePath] = { log: desiredLog };
15481
+ return true;
15482
+ }
15420
15483
  async function closeLogBaselineOnly(projectRoot, nodePath, lock) {
15421
15484
  const logBaseline = await computeLogBaselineForNode(projectRoot, nodePath);
15422
- if (!logBaseline) return;
15423
- const entry = lock.nodes[nodePath] ?? {};
15424
- entry.log = logBaseline;
15425
- lock.nodes[nodePath] = entry;
15485
+ if (!logBaseline) return false;
15486
+ const existing = lock.nodes[nodePath];
15487
+ if (logBaselineEquals(existing?.log, logBaseline)) return false;
15488
+ lock.nodes[nodePath] = { ...existing ?? {}, log: logBaseline };
15489
+ return true;
15426
15490
  }
15427
15491
 
15428
15492
  // src/core/fill-gc.ts
@@ -15885,6 +15949,7 @@ function formatOutput(result, view = { kind: "full" }) {
15885
15949
  }
15886
15950
  var ERROR_CODE_PRIORITY = [
15887
15951
  "lock-invalid",
15952
+ "log-entry-missing",
15888
15953
  "unverified",
15889
15954
  "aspect-violation-enforced",
15890
15955
  "prompt-too-large",
@@ -15999,7 +16064,7 @@ function renderErrorSection(errors) {
15999
16064
  const architecture = errors.filter((i) => ARCHITECTURE_CODES.has(i.code));
16000
16065
  const completeness = errors.filter((i) => COMPLETENESS_CODES.has(i.code));
16001
16066
  const strict = errors.filter((i) => STRICT_CODES.has(i.code));
16002
- const logErrors = errors.filter((i) => i.code === "log-conflict" || i.code === "log-integrity" || i.code === "log-format");
16067
+ const logErrors = errors.filter((i) => i.code === "log-conflict" || i.code === "log-integrity" || i.code === "log-format" || i.code === "log-entry-missing");
16003
16068
  const aspectErrors = errors.filter((i) => i.code === "aspect-violation-enforced");
16004
16069
  const remaining = errors.filter(
16005
16070
  (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)
@@ -17762,9 +17827,12 @@ prefix (e.g. \`src/handlers\`) covers that file or all files beneath it.
17762
17827
  ## log_required \u2014 when to enable the log gate
17763
17828
 
17764
17829
  Each node type may set \`log_required\` (default \`false\`). When \`true\`, a node of
17765
- that type demands a fresh log entry before \`yg check --approve\` whenever its
17766
- mapped source changed since the node's last positive closure (the log gate, see
17767
- \`yg knowledge read log-management\`).
17830
+ that type demands a fresh log entry whenever its mapped source changed since the
17831
+ node's last positive closure. The requirement is a property of the node TYPE plus
17832
+ a source change \u2014 independent of the node's aspects or pairs \u2014 and is enforced
17833
+ read-only: plain \`yg check\` flags a missing entry as a blocking error, so it
17834
+ bites even on a node that produces no pairs (the log gate, see \`yg knowledge read
17835
+ log-management\`).
17768
17836
 
17769
17837
  Enable it on types whose changes carry business intent worth capturing \u2014
17770
17838
  domain logic, command handlers, persistence adapters, anything where the WHY
@@ -19440,8 +19508,8 @@ pairs on that node \u2014 a legitimate vacuous pass, no verdict, no entry.
19440
19508
  }
19441
19509
  },
19442
19510
  "nodes": {
19443
- "billing/cancel": {
19444
- "source": "<sha256>", // source fingerprint \u2014 the log gate's basis
19511
+ "billing/cancel": { // present ONLY when log_required and/or a log.md exists
19512
+ "source": "<sha256>", // source fingerprint \u2014 log gate basis; ONLY for log_required nodes
19445
19513
  "log": { "last_entry_datetime": "<ISO>", "prefix_hash": "<sha256>" }
19446
19514
  }
19447
19515
  }
@@ -19463,6 +19531,16 @@ pairs on that node \u2014 a legitimate vacuous pass, no verdict, no entry.
19463
19531
  entirely.
19464
19532
  - \`nodes.<path>\` carries the source fingerprint (the log gate's contract,
19465
19533
  \`yg knowledge read log-management\`) and the append-only log integrity baseline.
19534
+ The source fingerprint is the log gate's drift basis, so it is recorded ONLY for
19535
+ \`log_required\` nodes \u2014 a non-log_required node gets a \`nodes\` entry only when it
19536
+ owns a \`log.md\` (then holding just the \`log\` baseline, no \`source\`).
19537
+ - Empty section \u21D2 no file. Each of the three split files is written ONLY when its
19538
+ section is non-empty; when empty it is not written at all (an existing empty husk
19539
+ is removed). So a repo with no LLM aspects has no \`yg-lock.nondeterministic.json\`,
19540
+ one with no \`log_required\` node and no \`log.md\` has no \`yg-lock.logs.json\`, and
19541
+ one with no deterministic aspects has no \`.yg-lock.deterministic.json\`. readLock
19542
+ treats an absent file as empty state, so this is transparent to every reader \u2014 a
19543
+ repo only carries the lock files it actually needs.
19466
19544
  - The built-in relation-conformance check is NOT stored in the lock \u2014 it is
19467
19545
  recomputed live on every \`yg check\`. The lock holds only aspect \`verdicts\`
19468
19546
  and per-node \`nodes\` facts; there is no relation section. See "Relation
@@ -20074,9 +20152,12 @@ real \`--approve\` bills at most that many calls. The preview always exits 0, ev
20074
20152
  when enforced pairs are unverified; it never blocks. Only a broken
20075
20153
  configuration (the step-1 structural gate) aborts the preview \u2014 it surfaces the
20076
20154
  same blocker a real \`--approve\` would hit. A cost estimate never demands a fresh
20077
- log entry, so the preview also bypasses the per-node log gate \u2014 it previews even
20078
- on \`log_required\` nodes whose source changed since their last closure, where the
20079
- real \`--approve\` would require the log entry first. \`--dry-run\` requires
20155
+ log entry, so the preview never HARD-STOPS on the per-node log gate \u2014 it previews
20156
+ the budget even on \`log_required\` nodes whose source changed since their last
20157
+ closure, where a real \`--approve\` would require the entry first. (The preview's
20158
+ trailing read-only check report still SURFACES that requirement as a
20159
+ \`log-entry-missing\` error, exactly as plain \`yg check\` does \u2014 it just exits 0
20160
+ and writes nothing.) \`--dry-run\` requires
20080
20161
  \`--approve\`; on its own it is a usage error (plain \`yg check\` is already a free,
20081
20162
  no-write read).
20082
20163
 
@@ -20319,7 +20400,7 @@ working-with-architecture\` is the home for that guidance). To know whether a no
20319
20400
  needs an entry, check its type in \`yg-architecture.yaml\`, or read the node's log
20320
20401
  state line in \`yg context --node\`.
20321
20402
 
20322
- A fresh log entry is required BEFORE \`yg check --approve\` whenever BOTH hold:
20403
+ A fresh log entry is required whenever BOTH hold:
20323
20404
 
20324
20405
  - the node's type has \`log_required: true\`, AND
20325
20406
  - the node's mapped source changed since its last positive closure (or this is the
@@ -20327,10 +20408,21 @@ A fresh log entry is required BEFORE \`yg check --approve\` whenever BOTH hold:
20327
20408
 
20328
20409
  "Fresh" means newer than the entry recorded at that closure \u2014 one fresh entry per
20329
20410
  closure cycle. The requirement depends ONLY on the type flag and the source
20330
- change. It is INDEPENDENT of aspect status. A node with no source change
20411
+ change. It is INDEPENDENT of aspect status AND of whether the node has any aspects
20412
+ or pairs at all \u2014 a node that owns source but has no effective (non-draft) aspects
20413
+ still needs an entry when its source changes. A node with no source change
20331
20414
  (cascade-only re-verification \u2014 an aspect was edited, the source untouched) needs
20332
20415
  no new entry.
20333
20416
 
20417
+ The requirement is enforced READ-ONLY. A missing entry is a blocking
20418
+ \`log-entry-missing\` error surfaced by plain \`yg check\` itself \u2014 computed live
20419
+ from each node's source fingerprint, like the relation-conformance check, at zero
20420
+ LLM cost \u2014 not merely a \`--approve\`-time gate. So a CI run on plain \`yg check\`
20421
+ catches an unlogged source change on a \`log_required\` node even when that node
20422
+ produces no pairs to fill. \`--approve\` only WRITES (records the closure baseline
20423
+ once the entry exists); it never gets a chance to bypass the requirement, because
20424
+ its own final re-check surfaces the same error.
20425
+
20334
20426
  ## Positive closure \u2014 the cycle
20335
20427
 
20336
20428
  Positive closure is the moment a \`yg check --approve\` run ends with every ENFORCED
@@ -20340,7 +20432,9 @@ records the node's source fingerprint and the log freshness baseline.
20340
20432
  Corollaries:
20341
20433
  - Advisory refusals do NOT prevent closure.
20342
20434
  - A node with only advisory/deterministic aspects, or no pairs at all, closes
20343
- vacuously.
20435
+ vacuously \u2014 BUT only once the log requirement is satisfied: a \`log_required\`
20436
+ node whose source changed with no fresh entry does not close, and \`yg check\`
20437
+ flags it red regardless of its (lack of) pairs.
20344
20438
  - A red enforced pair of either kind keeps the cycle OPEN \u2014 the same log entry
20345
20439
  stays valid through every retry until the node is actually green. Intent does
20346
20440
  not change between retries; only execution does.
@@ -20351,9 +20445,13 @@ The gate's "mapped source changed" test is computed from a per-node **source
20351
20445
  fingerprint** \u2014 one sha256 fold over the sorted \`[path, sha256(bytes)]\` list of
20352
20446
  ALL the node's mapped files (the full mapping, not the scope-filtered subject
20353
20447
  sets; binaries included by bytes). It lives in \`yg-lock.logs.json\` under
20354
- \`nodes.<path>.source\`, written at positive closure. The append-only log integrity
20355
- baseline (boundary datetime + prefix hash) lives beside it under
20356
- \`nodes.<path>.log\`. There is no separate per-node state file.
20448
+ \`nodes.<path>.source\`, written at positive closure \u2014 but ONLY for \`log_required\`
20449
+ nodes, since the fingerprint is the gate's drift basis and the gate never runs
20450
+ elsewhere. A non-log_required node records no \`source\`; it gets a \`nodes\` entry
20451
+ only when it owns a \`log.md\`, holding just the append-only \`log\` baseline
20452
+ (boundary datetime + prefix hash). When the section is empty (no log_required
20453
+ node, no \`log.md\`), \`yg-lock.logs.json\` is not written at all \u2014 an empty committed
20454
+ husk is removed. There is no separate per-node state file.
20357
20455
 
20358
20456
  The basic workflow:
20359
20457
 
@@ -20361,10 +20459,12 @@ The basic workflow:
20361
20459
  2. \`yg log add --node <path> --reason "<justification>"\`
20362
20460
  3. \`yg check --approve\`
20363
20461
 
20364
- If you forget step 2, the gate raises \`log-entry-missing\` and that node's pairs
20365
- are skipped (other nodes proceed); add the entry and re-run. If a pair is refused,
20366
- iterate on the code WITHOUT adding new log entries \u2014 one entry covers all edits
20367
- until the node reaches closure.
20462
+ If you forget step 2, plain \`yg check\` raises a blocking \`log-entry-missing\`
20463
+ error for that node (caught read-only, regardless of whether the node has pairs);
20464
+ at \`--approve\` a node that has pairs additionally has them skipped while other
20465
+ nodes proceed, and the run stays red until the entry exists. Add the entry and
20466
+ re-run. If a pair is refused, iterate on the code WITHOUT adding new log entries \u2014
20467
+ one entry covers all edits until the node reaches closure.
20368
20468
 
20369
20469
  ## Self-contained entry \u2014 worked example
20370
20470
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrisdudek/yg",
3
- "version": "5.2.1",
3
+ "version": "5.2.3",
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": {