@chrisdudek/yg 5.2.1 → 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.
- package/dist/bin.js +158 -61
- 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
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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
|
|
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");
|
|
@@ -15343,27 +15404,6 @@ async function runPairPool(items, concurrency, fn) {
|
|
|
15343
15404
|
}
|
|
15344
15405
|
|
|
15345
15406
|
// 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
15407
|
async function logGateBlocks(graph, projectRoot, node, lock, emitIssue) {
|
|
15368
15408
|
const blocked = await logGateBlocksNode(graph, projectRoot, node, lock);
|
|
15369
15409
|
if (!blocked) return false;
|
|
@@ -15386,6 +15426,12 @@ async function applyPositiveClosure(graph, projectRoot, lock, blockedNodes, pers
|
|
|
15386
15426
|
}
|
|
15387
15427
|
let mutated = false;
|
|
15388
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
|
+
}
|
|
15389
15435
|
let currentFingerprint;
|
|
15390
15436
|
try {
|
|
15391
15437
|
currentFingerprint = await computeSourceFingerprint(graph, nodePath);
|
|
@@ -15397,8 +15443,7 @@ async function applyPositiveClosure(graph, projectRoot, lock, blockedNodes, pers
|
|
|
15397
15443
|
throw e;
|
|
15398
15444
|
}
|
|
15399
15445
|
if (currentFingerprint === void 0) {
|
|
15400
|
-
await closeLogBaselineOnly(projectRoot, nodePath, lock);
|
|
15401
|
-
mutated = true;
|
|
15446
|
+
if (await closeLogBaselineOnly(projectRoot, nodePath, lock)) mutated = true;
|
|
15402
15447
|
continue;
|
|
15403
15448
|
}
|
|
15404
15449
|
const stored = lock.nodes[nodePath]?.source;
|
|
@@ -15417,12 +15462,32 @@ async function applyPositiveClosure(graph, projectRoot, lock, blockedNodes, pers
|
|
|
15417
15462
|
}
|
|
15418
15463
|
if (mutated) await persistLock();
|
|
15419
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
|
+
}
|
|
15420
15484
|
async function closeLogBaselineOnly(projectRoot, nodePath, lock) {
|
|
15421
15485
|
const logBaseline = await computeLogBaselineForNode(projectRoot, nodePath);
|
|
15422
|
-
if (!logBaseline) return;
|
|
15423
|
-
const
|
|
15424
|
-
|
|
15425
|
-
lock.nodes[nodePath] =
|
|
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;
|
|
15426
15491
|
}
|
|
15427
15492
|
|
|
15428
15493
|
// src/core/fill-gc.ts
|
|
@@ -15885,6 +15950,7 @@ function formatOutput(result, view = { kind: "full" }) {
|
|
|
15885
15950
|
}
|
|
15886
15951
|
var ERROR_CODE_PRIORITY = [
|
|
15887
15952
|
"lock-invalid",
|
|
15953
|
+
"log-entry-missing",
|
|
15888
15954
|
"unverified",
|
|
15889
15955
|
"aspect-violation-enforced",
|
|
15890
15956
|
"prompt-too-large",
|
|
@@ -15999,7 +16065,7 @@ function renderErrorSection(errors) {
|
|
|
15999
16065
|
const architecture = errors.filter((i) => ARCHITECTURE_CODES.has(i.code));
|
|
16000
16066
|
const completeness = errors.filter((i) => COMPLETENESS_CODES.has(i.code));
|
|
16001
16067
|
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");
|
|
16068
|
+
const logErrors = errors.filter((i) => i.code === "log-conflict" || i.code === "log-integrity" || i.code === "log-format" || i.code === "log-entry-missing");
|
|
16003
16069
|
const aspectErrors = errors.filter((i) => i.code === "aspect-violation-enforced");
|
|
16004
16070
|
const remaining = errors.filter(
|
|
16005
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)
|
|
@@ -17762,9 +17828,12 @@ prefix (e.g. \`src/handlers\`) covers that file or all files beneath it.
|
|
|
17762
17828
|
## log_required \u2014 when to enable the log gate
|
|
17763
17829
|
|
|
17764
17830
|
Each node type may set \`log_required\` (default \`false\`). When \`true\`, a node of
|
|
17765
|
-
that type demands a fresh log entry
|
|
17766
|
-
|
|
17767
|
-
|
|
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\`).
|
|
17768
17837
|
|
|
17769
17838
|
Enable it on types whose changes carry business intent worth capturing \u2014
|
|
17770
17839
|
domain logic, command handlers, persistence adapters, anything where the WHY
|
|
@@ -19440,8 +19509,8 @@ pairs on that node \u2014 a legitimate vacuous pass, no verdict, no entry.
|
|
|
19440
19509
|
}
|
|
19441
19510
|
},
|
|
19442
19511
|
"nodes": {
|
|
19443
|
-
"billing/cancel": {
|
|
19444
|
-
"source": "<sha256>", // source fingerprint \u2014
|
|
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
|
|
19445
19514
|
"log": { "last_entry_datetime": "<ISO>", "prefix_hash": "<sha256>" }
|
|
19446
19515
|
}
|
|
19447
19516
|
}
|
|
@@ -19463,6 +19532,12 @@ pairs on that node \u2014 a legitimate vacuous pass, no verdict, no entry.
|
|
|
19463
19532
|
entirely.
|
|
19464
19533
|
- \`nodes.<path>\` carries the source fingerprint (the log gate's contract,
|
|
19465
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.
|
|
19466
19541
|
- The built-in relation-conformance check is NOT stored in the lock \u2014 it is
|
|
19467
19542
|
recomputed live on every \`yg check\`. The lock holds only aspect \`verdicts\`
|
|
19468
19543
|
and per-node \`nodes\` facts; there is no relation section. See "Relation
|
|
@@ -20074,9 +20149,12 @@ real \`--approve\` bills at most that many calls. The preview always exits 0, ev
|
|
|
20074
20149
|
when enforced pairs are unverified; it never blocks. Only a broken
|
|
20075
20150
|
configuration (the step-1 structural gate) aborts the preview \u2014 it surfaces the
|
|
20076
20151
|
same blocker a real \`--approve\` would hit. A cost estimate never demands a fresh
|
|
20077
|
-
log entry, so the preview
|
|
20078
|
-
on \`log_required\` nodes whose source changed since their last
|
|
20079
|
-
real \`--approve\` would require the
|
|
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
|
|
20080
20158
|
\`--approve\`; on its own it is a usage error (plain \`yg check\` is already a free,
|
|
20081
20159
|
no-write read).
|
|
20082
20160
|
|
|
@@ -20319,7 +20397,7 @@ working-with-architecture\` is the home for that guidance). To know whether a no
|
|
|
20319
20397
|
needs an entry, check its type in \`yg-architecture.yaml\`, or read the node's log
|
|
20320
20398
|
state line in \`yg context --node\`.
|
|
20321
20399
|
|
|
20322
|
-
A fresh log entry is required
|
|
20400
|
+
A fresh log entry is required whenever BOTH hold:
|
|
20323
20401
|
|
|
20324
20402
|
- the node's type has \`log_required: true\`, AND
|
|
20325
20403
|
- the node's mapped source changed since its last positive closure (or this is the
|
|
@@ -20327,10 +20405,21 @@ A fresh log entry is required BEFORE \`yg check --approve\` whenever BOTH hold:
|
|
|
20327
20405
|
|
|
20328
20406
|
"Fresh" means newer than the entry recorded at that closure \u2014 one fresh entry per
|
|
20329
20407
|
closure cycle. The requirement depends ONLY on the type flag and the source
|
|
20330
|
-
change. It is INDEPENDENT of aspect status
|
|
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
|
|
20331
20411
|
(cascade-only re-verification \u2014 an aspect was edited, the source untouched) needs
|
|
20332
20412
|
no new entry.
|
|
20333
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
|
+
|
|
20334
20423
|
## Positive closure \u2014 the cycle
|
|
20335
20424
|
|
|
20336
20425
|
Positive closure is the moment a \`yg check --approve\` run ends with every ENFORCED
|
|
@@ -20340,7 +20429,9 @@ records the node's source fingerprint and the log freshness baseline.
|
|
|
20340
20429
|
Corollaries:
|
|
20341
20430
|
- Advisory refusals do NOT prevent closure.
|
|
20342
20431
|
- A node with only advisory/deterministic aspects, or no pairs at all, closes
|
|
20343
|
-
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.
|
|
20344
20435
|
- A red enforced pair of either kind keeps the cycle OPEN \u2014 the same log entry
|
|
20345
20436
|
stays valid through every retry until the node is actually green. Intent does
|
|
20346
20437
|
not change between retries; only execution does.
|
|
@@ -20351,9 +20442,13 @@ The gate's "mapped source changed" test is computed from a per-node **source
|
|
|
20351
20442
|
fingerprint** \u2014 one sha256 fold over the sorted \`[path, sha256(bytes)]\` list of
|
|
20352
20443
|
ALL the node's mapped files (the full mapping, not the scope-filtered subject
|
|
20353
20444
|
sets; binaries included by bytes). It lives in \`yg-lock.logs.json\` under
|
|
20354
|
-
\`nodes.<path>.source\`, written at positive closure
|
|
20355
|
-
|
|
20356
|
-
|
|
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.
|
|
20357
20452
|
|
|
20358
20453
|
The basic workflow:
|
|
20359
20454
|
|
|
@@ -20361,10 +20456,12 @@ The basic workflow:
|
|
|
20361
20456
|
2. \`yg log add --node <path> --reason "<justification>"\`
|
|
20362
20457
|
3. \`yg check --approve\`
|
|
20363
20458
|
|
|
20364
|
-
If you forget step 2,
|
|
20365
|
-
|
|
20366
|
-
|
|
20367
|
-
until the
|
|
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.
|
|
20368
20465
|
|
|
20369
20466
|
## Self-contained entry \u2014 worked example
|
|
20370
20467
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrisdudek/yg",
|
|
3
|
-
"version": "5.2.
|
|
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": {
|