@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.
- package/dist/bin.js +165 -65
- 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;
|
|
@@ -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
|
-
|
|
3606
|
-
await writeFileIfChanged(detLockPath(yggRoot), content20);
|
|
3624
|
+
await writeOrRemoveSplitFile(detLockPath(yggRoot), lock.version, det, {});
|
|
3607
3625
|
return;
|
|
3608
3626
|
}
|
|
3609
|
-
await
|
|
3610
|
-
await
|
|
3611
|
-
await
|
|
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
|
|
15424
|
-
|
|
15425
|
-
lock.nodes[nodePath] =
|
|
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
|
|
17766
|
-
|
|
17767
|
-
|
|
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
|
|
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
|
|
20078
|
-
on \`log_required\` nodes whose source changed since their last
|
|
20079
|
-
real \`--approve\` would require the
|
|
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
|
|
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
|
|
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
|
|
20355
|
-
|
|
20356
|
-
|
|
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,
|
|
20365
|
-
|
|
20366
|
-
|
|
20367
|
-
until the
|
|
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.
|
|
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": {
|