@webpieces/ai-hook-rules 0.4.630 → 0.4.632
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/package.json +2 -2
- package/src/adapters/claude-code-response.js +16 -1
- package/src/adapters/claude-code-response.js.map +1 -1
- package/src/adapters/hook-core.js +1 -1
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/l0-allowlist.d.ts +6 -2
- package/src/bin/l0-allowlist.js +60 -16
- package/src/bin/l0-allowlist.js.map +1 -1
- package/src/bin/shim-audit-log.d.ts +4 -3
- package/src/bin/shim-audit-log.js +40 -12
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim-deny-reason.d.ts +0 -5
- package/src/bin/shim-deny-reason.js +199 -108
- package/src/bin/shim-deny-reason.js.map +1 -1
- package/src/bin/shim-drift-fix.d.ts +1 -0
- package/src/bin/shim-drift-fix.js +53 -0
- package/src/bin/shim-drift-fix.js.map +1 -0
- package/src/bin/shim-testkit.d.ts +11 -0
- package/src/bin/shim-testkit.js +15 -0
- package/src/bin/shim-testkit.js.map +1 -1
- package/src/bin/shim.js +58 -15
- package/src/bin/shim.js.map +1 -1
- package/src/core/decision-log.d.ts +12 -4
- package/src/core/decision-log.js +13 -5
- package/src/core/decision-log.js.map +1 -1
- package/src/core/l0-fault-codes.d.ts +46 -0
- package/src/core/l0-fault-codes.js +55 -1
- package/src/core/l0-fault-codes.js.map +1 -1
- package/src/core/l0-matrix.d.ts +23 -4
- package/src/core/l0-matrix.js +74 -24
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/runner.js +1 -1
- package/src/core/runner.js.map +1 -1
- package/templates/ai-hook.sh +73 -23
package/src/core/decision-log.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.invocationLog = exports.InvocationLog = exports.GuardInvocation = exports.GuardDecision = exports.MATRIX_L2 = exports.
|
|
3
|
+
exports.invocationLog = exports.InvocationLog = exports.GuardInvocation = exports.GuardDecision = exports.MATRIX_L2 = exports.MATRIX_L0_BLOCK = exports.MATRIX_L0_ALLOW = exports.MatrixRef = void 0;
|
|
4
4
|
exports.logGuardDecision = logGuardDecision;
|
|
5
5
|
exports.logL1Decision = logL1Decision;
|
|
6
6
|
exports.branchForLog = branchForLog;
|
|
@@ -39,15 +39,23 @@ class MatrixRef {
|
|
|
39
39
|
}
|
|
40
40
|
exports.MatrixRef = MatrixRef;
|
|
41
41
|
/**
|
|
42
|
-
* The layer tokens. `row` is `'-'` for a layer with no row array YET (L2 is the un-converted one
|
|
43
|
-
*
|
|
44
|
-
* `grep layer=L2` works today and the row fills in when L2 converts.
|
|
42
|
+
* The layer tokens. `row` is `'-'` only for a layer with no row array YET (L2 is the un-converted one)
|
|
43
|
+
* — but the LAYER is always named, so `grep layer=L2` works today and the row fills in when L2 converts.
|
|
45
44
|
*
|
|
46
45
|
* These are REQUIRED at the constructor, not defaulted: a defaulted `MatrixRef` would make the
|
|
47
46
|
* uncited case reachable by doing nothing and impossible to grep — the same defect this file's own
|
|
48
47
|
* docblock argues against for `'BLOCK'`, where silence was the one wrong answer available.
|
|
48
|
+
*
|
|
49
|
+
* L0 NOW CITES ITS ROW TOO. It used to be `'-'` on the grounds that "L0's faults are a table of letters
|
|
50
|
+
* rather than numbered rows" — but L0 has BOTH: the letter says WHICH fault, and the numbered decision
|
|
51
|
+
* matrix (`renderGuardMatrixDoc`) says which of its three rows was taken. There is one ALLOW row and one
|
|
52
|
+
* BLOCK row, so the pair `row=` + `fault=` pins the decision exactly, and the deny an agent reads now
|
|
53
|
+
* carries the identical pair. That is the join: one grep spans the deny, the log line and the doc.
|
|
54
|
+
* Two constants, not one `MATRIX_L0`, because the two call sites are a cure-bypass ALLOW and an L0
|
|
55
|
+
* BLOCK — one token covering both is what made the row unciteable in the first place.
|
|
49
56
|
*/
|
|
50
|
-
exports.
|
|
57
|
+
exports.MATRIX_L0_ALLOW = new MatrixRef('L0', l0_fault_codes_1.L0_ROW_ALLOWLISTED);
|
|
58
|
+
exports.MATRIX_L0_BLOCK = new MatrixRef('L0', l0_fault_codes_1.L0_ROW_BLOCKED);
|
|
51
59
|
exports.MATRIX_L2 = new MatrixRef('L2', '-');
|
|
52
60
|
// Data-only record of one guard decision (per CLAUDE.md: classes for data, not object literals).
|
|
53
61
|
// `cache` summarizes the async-written main-sync-status.json that drove a feature-branch-guard
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decision-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/decision-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { dotWebpieces, readMainSyncStatus, MainSyncStatus, RepoRootFinder, claudeEnv } from '@webpieces/rules-config';\nimport { L1_LOCATION_STREAM, L2_DECISIONS_STREAM, CALLS_STREAM } from './log-streams';\n\nimport { L0_FAULT_NONE } from './l0-fault-codes';\nimport { toError } from './to-error';\nimport { logStream } from './log-stream';\n\n// The SYNC decision log — what the synchronous hook DID on each invocation and WHY. Its companion is\n// the ASYNC log (the `async-refresh/` stream, written by the detached refresher in main-sync-log.ts). This\n// one records EVERY guard decision — allow, block, config-bypass, and the fail-open cases — and CITES\n// the async-written cache snapshot (`cache` field) that drove the decision, so a wrong allow/block is\n// traceable to a stale or missing async write. Writes to `.webpieces/logs/L2-decisions/<writer>.log` —\n// the LAYER is the directory, the WRITER is the file (see log-streams.ts and LogStream).\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded (mirrors rejection-log)\nconst MAX_TARGET_LEN = 160;\n\n/**\n * THE ACTION CODEBOOK, as a type. These are the five actions GUARD_MATRIX.md numbers 1-5, and they\n * are the vocabulary EVERY layer reports in — so one grep spans L-1, L0, L1 and L2.\n *\n * The three distinctions this exists to make, none of which `'ALLOW' | 'BLOCK'` could:\n *\n * ALLOW no objection — the call was HANDED DOWN to the next layer. A layer saying ALLOW\n * is NOT saying the call ran: the layer below it, or the OTHER parallel hook, may\n * still deny. This is L1's `ACT_DOWN`.\n * ALLOW_EXEMPT out of scope by construction — allowed, and evaluation STOPS here. L1's\n * `ACT_EXEMPT`.\n * ALLOW_FAIL_OPEN state could not be established, so nothing was judged. Keeping this distinct\n * from ALLOW is the entire point of the type: a fail-open allow and a real allow\n * that look identical make it impossible to tell whether the guards are protecting\n * anything or quietly abstaining. It used to be a `' (fail-open)'` SUBSTRING on the\n * reason field, which is exactly why the abstentions were never countable.\n * BLOCK_AI_CURE blocked, and the printed cure is a command the AI can run itself.\n * BLOCK_HUMAN blocked, and it needs a human decision — or a delegation (spawn a subagent) that\n * the blocked agent cannot perform for itself.\n *\n * Hard cut, per CLAUDE.md: `'BLOCK'` is GONE rather than aliased, so every construction site fails to\n * compile and has to say which kind of block it is. Before, that question had exactly one wrong\n * answer available — silence.\n */\nexport type Verdict = 'ALLOW' | 'ALLOW_EXEMPT' | 'ALLOW_FAIL_OPEN' | 'BLOCK_AI_CURE' | 'BLOCK_HUMAN';\n\n/**\n * WHICH ROW of WHICH layer's decision table produced this line. Data-only → a class, per CLAUDE.md.\n *\n * `row` is the row NUMBER from the layer's row array (`L1_ROWS[i].num`) — the\n * same number the generated doc prints, because the doc is rendered from that same array. So a log\n * line joins to its matrix row BY NUMBER, and checking observed behaviour against the documented use\n * cases becomes a lookup rather than an investigation. `'-'` for a layer with no row array yet (L2).\n */\nexport class MatrixRef {\n constructor(readonly layer: string, readonly row: string) {}\n}\n\n/**\n * The layer tokens. `row` is `'-'` for a layer with no row array YET (L2 is the un-converted one, and\n * L0's faults are a table of letters rather than numbered rows) — but the LAYER is always named, so\n * `grep layer=L2` works today and the row fills in when L2 converts.\n *\n * These are REQUIRED at the constructor, not defaulted: a defaulted `MatrixRef` would make the\n * uncited case reachable by doing nothing and impossible to grep — the same defect this file's own\n * docblock argues against for `'BLOCK'`, where silence was the one wrong answer available.\n */\nexport const MATRIX_L0 = new MatrixRef('L0', '-');\nexport const MATRIX_L2 = new MatrixRef('L2', '-');\n\n// Data-only record of one guard decision (per CLAUDE.md: classes for data, not object literals).\n// `cache` summarizes the async-written main-sync-status.json that drove a feature-branch-guard\n// decision (branch/merged/conflict/fork + the cache timestamp), or '-' when no cache was consulted\n// (bash guards, on-main, config-bypass).\nexport class GuardDecision {\n rule: string;\n tool: string;\n target: string; // file path (file guards) or the bash command (bash guards)\n branch: string;\n verdict: Verdict;\n reason: string;\n cache: string;\n /**\n * The L0 fault this decision IS, in the codebook's letter (core/l0-fault-codes.ts), or `-` for an\n * ordinary rule decision. The `sh` shim has always stamped `fault=` on its own stream; the three\n * JS-side faults (S/C/Y) reached this one with no label at all, so `grep fault=S` found nothing\n * even while an S storm was blocking every call.\n */\n fault: string;\n /** Which layer + row decided this. See MatrixRef — it is what joins a log line to the doc. */\n matrix: MatrixRef;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(rule: string, tool: string, target: string, branch: string, verdict: Verdict, reason: string, cache: string = '-', fault: string, matrix: MatrixRef) {\n this.rule = rule;\n this.tool = tool;\n this.target = target;\n this.branch = branch;\n this.verdict = verdict;\n this.reason = reason;\n this.cache = cache;\n this.fault = fault;\n this.matrix = matrix;\n }\n}\n\n/**\n * Append one tab-separated line per L2 decision to `.webpieces/logs/L2-decisions/<writer>.log`, where\n * <writer> is LogStream's `<sessionId>-<agentId|coordinator>-<hook>` key (a caller that never\n * identified renders as `unknown-coordinator-hook` — there is no un-keyed name).\n * `root` is\n * the repo/workspace root that holds `.webpieces` (callers pass workspaceRoot, or a\n * RepoRootFinder-resolved root at the pre-load config-bypass site — never a raw cwd, so a bypass\n * logged from a subdir never scatters a stray `.webpieces`). Swallows all errors — logging must never\n * block or fail a hook.\n */\n// webpieces-disable no-function-outside-class -- the module-scope writer this log has always been, beside branchForLog/oneLine/rotateLogFile; it must stay callable from a tree too broken to build a DI container\nexport function logGuardDecision(root: string, decision: GuardDecision): void {\n appendDecision(root, L2_DECISIONS_STREAM, decision);\n}\n\n/**\n * The L1 stream — `.webpieces/logs/L1-location/<writer>.log`.\n *\n * L1 had NO stream. Its three blocking paths wrote into L2's file under an implementation name\n * (`force-to-root`, `trinary-version-skew`, `cd-must-be-first`), and its NON-blocking outcomes —\n * the exempt row and the three hand-down rows — wrote nothing at all. So \"L1 had no objection\" was\n * unobservable, and \"show me every L1 decision\" had no answer: L1 existed in the trail only as the\n * `root=` / `projectDir=` / `tree=` columns stapled onto somebody else's line.\n *\n * A SIBLING rather than a `base` parameter on logGuardDecision, deliberately: that signature is what\n * the process-wide `logStream` singleton exists to keep unchanged (see LogStream's docblock), and\n * `INVOCATION_LOG_FILE` already establishes the pattern of a second stream owning its own name in\n * this same module.\n */\n// webpieces-disable no-function-outside-class -- sibling of logGuardDecision, same module-scope writer shape and same reason\nexport function logL1Decision(root: string, decision: GuardDecision): void {\n appendDecision(root, L1_LOCATION_STREAM, decision);\n}\n\n// The one appender both streams share. `streamDir` is the LAYER; the writer key inside it is\n// logStream's session/agent/hook, which is what keeps one writer per file.\n// webpieces-disable no-function-outside-class -- the shared body of the two module-scope writers above\nfunction appendDecision(root: string, streamDir: string, decision: GuardDecision): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const timestamp = new Date().toISOString();\n // LOCAL scope: a guard decision belongs to the tree it judged. WHO made the call is answered\n // by the filename, which logStream names with session/agent/hook.\n const logsDir = dotWebpieces.logsFile(root, streamDir);\n fs.mkdirSync(logsDir, { recursive: true });\n\n const logPath = path.join(logsDir, logStream.writerFile('.log'));\n rotateLogFile(logPath, path.join(logsDir, logStream.writerFile('.1.log')));\n\n const line = [\n `[${timestamp}]`,\n decision.verdict,\n decision.tool,\n oneLine(decision.target),\n decision.branch,\n decision.rule,\n oneLine(decision.reason),\n oneLine(decision.cache),\n // WHICH ROW of WHICH table decided this. The directory already carries the layer, but a\n // line quoted out of its file must still say what judged it — and `row=` is the join key\n // to the generated doc, which is the point of the whole exercise.\n `layer=${decision.matrix.layer}`,\n `row=${decision.matrix.row}`,\n // The tree this decision was actually made against, and what Claude Code told the hook the\n // project was. Appended (never reordered) for the same reason as on the invocation line —\n // see ClaudeEnv: when these two disagree, that disagreement is the bug.\n `root=${root}`,\n `projectDir=${claudeEnv.projectDirForLog()}`,\n // git's name for that tree — `primary`, else the worktree name. Same literal and same\n // derivation as the L0 shim log's `tree=` (shim-audit-log.ts), so one grep spans both\n // streams: L0 carries tree without projectDir, L1 now carries both.\n `tree=${dotWebpieces.worktreeName(root) || 'primary'}`,\n // APPEND-ONLY, same spelling as the invocation line and the L0 shim log: which L0 fault\n // this was, or `-`.\n `fault=${decision.fault}`,\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n\n/**\n * What the guard SAW on one invocation, captured up front and held until the outcome is known.\n * Data-only (per CLAUDE.md: classes for data, explicit construction).\n */\nexport class GuardInvocation {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n public readonly root: string,\n public readonly timestamp: string,\n public readonly tool: string,\n public readonly target: string,\n public readonly branch: string,\n public readonly sync: string,\n public readonly projectDir: string,\n ) {}\n}\n\n/**\n * The per-INVOCATION stream — `.webpieces/logs/calls/<writer>.log` (see LogStream for the writer\n * key), one line for EVERY guards-hook\n * call (allow or block, bash or file), unlike `L2-decisions/` which records only the calls a\n * rule actually judged. It captures the tool, the command/file, the live git branch, the async-written\n * main-sync-status.json snapshot (branch / merged / fork-point / conflict), and — since this class\n * replaced a bare log-and-forget function — HOW THE CALL ENDED.\n *\n * WHY IT IS TWO CALLS. The line used to be written the moment the hook started, so it could not carry\n * a verdict: the decision had not been made yet. Answering \"what happened to this call?\" therefore\n * meant joining this file against the L2 decision stream BY TIMESTAMP, which is exactly the kind of\n * reconstruction a log exists to make unnecessary. So {@link begin} now only CAPTURES (including the\n * git/cache reads, which must still happen while the hook is running), and {@link finish} — called\n * from the hook's single terminal boundary, emitAllow/emitDeny — writes the whole line once the\n * outcome is known. The two streams stay distinct in purpose: this one is \"every call and how it\n * ended\", the decision log remains \"every judgement and why\".\n *\n * Every error is swallowed: logging must never block or fail a hook.\n */\nexport class InvocationLog {\n private pending: GuardInvocation | null = null;\n\n /**\n * Capture the context of one invocation. `cwd` is the AI's working dir; the repo root that owns\n * `.webpieces` is resolved from it. Writes NOTHING — {@link finish} does that.\n */\n begin(cwd: string, tool: string, target: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const branch = branchForLog(root);\n // The cache is branch-keyed, so the entry to log is the one for the branch we are standing\n // on. 'unknown' (branchForLog's failure value) simply misses and logs 'sync=none'.\n const sync = summarizeSyncStatus(readMainSyncStatus(root, branch));\n this.pending = new GuardInvocation(root, new Date().toISOString(), tool, oneLine(target), branch, sync, claudeEnv.projectDirForLog());\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n /**\n * Write the captured line, now stamped with the outcome. A no-op when nothing was captured (the\n * 'rules' hook, or a terminal boundary reached before begin()), and it clears the pending entry so\n * a second emit cannot double-log.\n *\n * `rule` is the rule that blocked, or '-' when there is none; `fault` is the L0 fault code when this\n * call ended on one (S/C/Y — the JS-side faults), else '-'. FIELD ORDER IS APPEND-ONLY: the five\n * original fields keep their positions (cleanup automation mines this file), and `guards=` /\n * `rule=` / … / `fault=` are added at the end.\n */\n finish(verdict: Verdict, rule: string, fault: string = L0_FAULT_NONE): void {\n const invocation = this.pending;\n this.pending = null;\n if (invocation === null) return;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const logsDir = dotWebpieces.logsFile(invocation.root, CALLS_STREAM);\n fs.mkdirSync(logsDir, { recursive: true });\n const logPath = path.join(logsDir, logStream.writerFile('.log'));\n rotateLogFile(logPath, path.join(logsDir, logStream.writerFile('.1.log')));\n\n const line = [\n `[${invocation.timestamp}]`,\n invocation.tool,\n invocation.target,\n `branch=${invocation.branch}`,\n invocation.sync,\n // `guards=`, NOT `verdict=`. This hook can only report on ITSELF. Claude Code runs all\n // its PreToolUse hooks IN PARALLEL, so another hook process may deny a call this one\n // had no objection to, and neither can see the other's answer. Measured under the\n // RETIRED three-hook form: `cd <repo>/packages && ls` was DENIED by L-1 and recorded\n // here three times as `verdict=ALLOW`. The old field name promised an outcome it\n // structurally cannot know, so the name stayed even though L-1 is gone: the two\n // surviving hooks still run in parallel and still cannot see each other.\n `guards=${verdict}`,\n `rule=${oneLine(rule) || '-'}`,\n // The tree the guard ACTED in, next to what Claude Code said the project was. Both, on\n // every line, because the diagnostic value is entirely in comparing them — see\n // ClaudeEnv for the open question this field exists to settle empirically.\n `root=${invocation.root}`,\n `projectDir=${invocation.projectDir}`,\n // See logGuardDecision: the short tree label, so `tree=primary` with a matching\n // projectDir reads as healthy at a glance and `tree=<worktree>` beside a projectDir\n // pointing at the primary is the straddle, without diffing two absolute paths.\n `tree=${dotWebpieces.worktreeName(invocation.root) || 'primary'}`,\n // WHICH L0 fault ended this call, in the same letters and the same field name the L0 sh\n // shim uses (the `L0-shim/` stream) — so ONE grep spans the whole trail.\n `fault=${fault}`,\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\n// Process-wide instance: one hook process handles exactly one tool call, so a single pending entry is\n// the whole state there is. Module-scope (rather than DI) because the terminal boundary that flushes\n// it — emitAllow/emitDeny — is itself module-scope protocol code with no container in reach.\nexport const invocationLog = new InvocationLog();\n\n// One-field summary of main-sync-status.json for the invocation log: the branch the cache is FOR,\n// whether it is already merged (and its PR), fork-point presence, and conflict state — the signals a\n// cleanup step keys off. 'sync=none' when the cache has not been written yet (first call of a session).\nfunction summarizeSyncStatus(status: MainSyncStatus | null): string {\n if (status === null) return 'sync=none';\n const merged = status.branchAlreadyMerged ? `PR#${status.mergedPr !== '' ? status.mergedPr : '?'}` : 'no';\n return `sync=${status.branch} merged=${merged} fork=${String(status.hasForkPoint)} conflict=${String(status.conflict)} ts=${status.timestamp}`;\n}\n\n// Best-effort current branch for the log line. Returns 'unknown' on any failure (e.g. not a git\n// repo) — this is for display only, never for a control decision.\nexport function branchForLog(root: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: root,\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim() || 'unknown';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return 'unknown';\n }\n}\n\n// Collapse newlines/tabs and cap length so one decision is always one log line.\nfunction oneLine(value: string): string {\n const flat = value.replace(/[\\t\\r\\n]+/g, ' ').trim();\n return flat.length <= MAX_TARGET_LEN ? flat : flat.slice(0, MAX_TARGET_LEN) + '…';\n}\n\nfunction rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"decision-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/decision-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nimport { dotWebpieces, readMainSyncStatus, MainSyncStatus, RepoRootFinder, claudeEnv } from '@webpieces/rules-config';\nimport { L1_LOCATION_STREAM, L2_DECISIONS_STREAM, CALLS_STREAM } from './log-streams';\n\nimport { L0_FAULT_NONE, L0_ROW_ALLOWLISTED, L0_ROW_BLOCKED } from './l0-fault-codes';\nimport { toError } from './to-error';\nimport { logStream } from './log-stream';\n\n// The SYNC decision log — what the synchronous hook DID on each invocation and WHY. Its companion is\n// the ASYNC log (the `async-refresh/` stream, written by the detached refresher in main-sync-log.ts). This\n// one records EVERY guard decision — allow, block, config-bypass, and the fail-open cases — and CITES\n// the async-written cache snapshot (`cache` field) that drove the decision, so a wrong allow/block is\n// traceable to a stale or missing async write. Writes to `.webpieces/logs/L2-decisions/<writer>.log` —\n// the LAYER is the directory, the WRITER is the file (see log-streams.ts and LogStream).\nconst MAX_LOG_BYTES = 512 * 1024; // 512 KB — rotate when exceeded (mirrors rejection-log)\nconst MAX_TARGET_LEN = 160;\n\n/**\n * THE ACTION CODEBOOK, as a type. These are the five actions GUARD_MATRIX.md numbers 1-5, and they\n * are the vocabulary EVERY layer reports in — so one grep spans L-1, L0, L1 and L2.\n *\n * The three distinctions this exists to make, none of which `'ALLOW' | 'BLOCK'` could:\n *\n * ALLOW no objection — the call was HANDED DOWN to the next layer. A layer saying ALLOW\n * is NOT saying the call ran: the layer below it, or the OTHER parallel hook, may\n * still deny. This is L1's `ACT_DOWN`.\n * ALLOW_EXEMPT out of scope by construction — allowed, and evaluation STOPS here. L1's\n * `ACT_EXEMPT`.\n * ALLOW_FAIL_OPEN state could not be established, so nothing was judged. Keeping this distinct\n * from ALLOW is the entire point of the type: a fail-open allow and a real allow\n * that look identical make it impossible to tell whether the guards are protecting\n * anything or quietly abstaining. It used to be a `' (fail-open)'` SUBSTRING on the\n * reason field, which is exactly why the abstentions were never countable.\n * BLOCK_AI_CURE blocked, and the printed cure is a command the AI can run itself.\n * BLOCK_HUMAN blocked, and it needs a human decision — or a delegation (spawn a subagent) that\n * the blocked agent cannot perform for itself.\n *\n * Hard cut, per CLAUDE.md: `'BLOCK'` is GONE rather than aliased, so every construction site fails to\n * compile and has to say which kind of block it is. Before, that question had exactly one wrong\n * answer available — silence.\n */\nexport type Verdict = 'ALLOW' | 'ALLOW_EXEMPT' | 'ALLOW_FAIL_OPEN' | 'BLOCK_AI_CURE' | 'BLOCK_HUMAN';\n\n/**\n * WHICH ROW of WHICH layer's decision table produced this line. Data-only → a class, per CLAUDE.md.\n *\n * `row` is the row NUMBER from the layer's row array (`L1_ROWS[i].num`) — the\n * same number the generated doc prints, because the doc is rendered from that same array. So a log\n * line joins to its matrix row BY NUMBER, and checking observed behaviour against the documented use\n * cases becomes a lookup rather than an investigation. `'-'` for a layer with no row array yet (L2).\n */\nexport class MatrixRef {\n constructor(readonly layer: string, readonly row: string) {}\n}\n\n/**\n * The layer tokens. `row` is `'-'` only for a layer with no row array YET (L2 is the un-converted one)\n * — but the LAYER is always named, so `grep layer=L2` works today and the row fills in when L2 converts.\n *\n * These are REQUIRED at the constructor, not defaulted: a defaulted `MatrixRef` would make the\n * uncited case reachable by doing nothing and impossible to grep — the same defect this file's own\n * docblock argues against for `'BLOCK'`, where silence was the one wrong answer available.\n *\n * L0 NOW CITES ITS ROW TOO. It used to be `'-'` on the grounds that \"L0's faults are a table of letters\n * rather than numbered rows\" — but L0 has BOTH: the letter says WHICH fault, and the numbered decision\n * matrix (`renderGuardMatrixDoc`) says which of its three rows was taken. There is one ALLOW row and one\n * BLOCK row, so the pair `row=` + `fault=` pins the decision exactly, and the deny an agent reads now\n * carries the identical pair. That is the join: one grep spans the deny, the log line and the doc.\n * Two constants, not one `MATRIX_L0`, because the two call sites are a cure-bypass ALLOW and an L0\n * BLOCK — one token covering both is what made the row unciteable in the first place.\n */\nexport const MATRIX_L0_ALLOW = new MatrixRef('L0', L0_ROW_ALLOWLISTED);\nexport const MATRIX_L0_BLOCK = new MatrixRef('L0', L0_ROW_BLOCKED);\nexport const MATRIX_L2 = new MatrixRef('L2', '-');\n\n// Data-only record of one guard decision (per CLAUDE.md: classes for data, not object literals).\n// `cache` summarizes the async-written main-sync-status.json that drove a feature-branch-guard\n// decision (branch/merged/conflict/fork + the cache timestamp), or '-' when no cache was consulted\n// (bash guards, on-main, config-bypass).\nexport class GuardDecision {\n rule: string;\n tool: string;\n target: string; // file path (file guards) or the bash command (bash guards)\n branch: string;\n verdict: Verdict;\n reason: string;\n cache: string;\n /**\n * The L0 fault this decision IS, in the codebook's letter (core/l0-fault-codes.ts), or `-` for an\n * ordinary rule decision. The `sh` shim has always stamped `fault=` on its own stream; the three\n * JS-side faults (S/C/Y) reached this one with no label at all, so `grep fault=S` found nothing\n * even while an S storm was blocking every call.\n */\n fault: string;\n /** Which layer + row decided this. See MatrixRef — it is what joins a log line to the doc. */\n matrix: MatrixRef;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(rule: string, tool: string, target: string, branch: string, verdict: Verdict, reason: string, cache: string = '-', fault: string, matrix: MatrixRef) {\n this.rule = rule;\n this.tool = tool;\n this.target = target;\n this.branch = branch;\n this.verdict = verdict;\n this.reason = reason;\n this.cache = cache;\n this.fault = fault;\n this.matrix = matrix;\n }\n}\n\n/**\n * Append one tab-separated line per L2 decision to `.webpieces/logs/L2-decisions/<writer>.log`, where\n * <writer> is LogStream's `<sessionId>-<agentId|coordinator>-<hook>` key (a caller that never\n * identified renders as `unknown-coordinator-hook` — there is no un-keyed name).\n * `root` is\n * the repo/workspace root that holds `.webpieces` (callers pass workspaceRoot, or a\n * RepoRootFinder-resolved root at the pre-load config-bypass site — never a raw cwd, so a bypass\n * logged from a subdir never scatters a stray `.webpieces`). Swallows all errors — logging must never\n * block or fail a hook.\n */\n// webpieces-disable no-function-outside-class -- the module-scope writer this log has always been, beside branchForLog/oneLine/rotateLogFile; it must stay callable from a tree too broken to build a DI container\nexport function logGuardDecision(root: string, decision: GuardDecision): void {\n appendDecision(root, L2_DECISIONS_STREAM, decision);\n}\n\n/**\n * The L1 stream — `.webpieces/logs/L1-location/<writer>.log`.\n *\n * L1 had NO stream. Its three blocking paths wrote into L2's file under an implementation name\n * (`force-to-root`, `trinary-version-skew`, `cd-must-be-first`), and its NON-blocking outcomes —\n * the exempt row and the three hand-down rows — wrote nothing at all. So \"L1 had no objection\" was\n * unobservable, and \"show me every L1 decision\" had no answer: L1 existed in the trail only as the\n * `root=` / `projectDir=` / `tree=` columns stapled onto somebody else's line.\n *\n * A SIBLING rather than a `base` parameter on logGuardDecision, deliberately: that signature is what\n * the process-wide `logStream` singleton exists to keep unchanged (see LogStream's docblock), and\n * `INVOCATION_LOG_FILE` already establishes the pattern of a second stream owning its own name in\n * this same module.\n */\n// webpieces-disable no-function-outside-class -- sibling of logGuardDecision, same module-scope writer shape and same reason\nexport function logL1Decision(root: string, decision: GuardDecision): void {\n appendDecision(root, L1_LOCATION_STREAM, decision);\n}\n\n// The one appender both streams share. `streamDir` is the LAYER; the writer key inside it is\n// logStream's session/agent/hook, which is what keeps one writer per file.\n// webpieces-disable no-function-outside-class -- the shared body of the two module-scope writers above\nfunction appendDecision(root: string, streamDir: string, decision: GuardDecision): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const timestamp = new Date().toISOString();\n // LOCAL scope: a guard decision belongs to the tree it judged. WHO made the call is answered\n // by the filename, which logStream names with session/agent/hook.\n const logsDir = dotWebpieces.logsFile(root, streamDir);\n fs.mkdirSync(logsDir, { recursive: true });\n\n const logPath = path.join(logsDir, logStream.writerFile('.log'));\n rotateLogFile(logPath, path.join(logsDir, logStream.writerFile('.1.log')));\n\n const line = [\n `[${timestamp}]`,\n decision.verdict,\n decision.tool,\n oneLine(decision.target),\n decision.branch,\n decision.rule,\n oneLine(decision.reason),\n oneLine(decision.cache),\n // WHICH ROW of WHICH table decided this. The directory already carries the layer, but a\n // line quoted out of its file must still say what judged it — and `row=` is the join key\n // to the generated doc, which is the point of the whole exercise.\n `layer=${decision.matrix.layer}`,\n `row=${decision.matrix.row}`,\n // The tree this decision was actually made against, and what Claude Code told the hook the\n // project was. Appended (never reordered) for the same reason as on the invocation line —\n // see ClaudeEnv: when these two disagree, that disagreement is the bug.\n `root=${root}`,\n `projectDir=${claudeEnv.projectDirForLog()}`,\n // git's name for that tree — `primary`, else the worktree name. Same literal and same\n // derivation as the L0 shim log's `tree=` (shim-audit-log.ts), so one grep spans both\n // streams: L0 carries tree without projectDir, L1 now carries both.\n `tree=${dotWebpieces.worktreeName(root) || 'primary'}`,\n // APPEND-ONLY, same spelling as the invocation line and the L0 shim log: which L0 fault\n // this was, or `-`.\n `fault=${decision.fault}`,\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n\n/**\n * What the guard SAW on one invocation, captured up front and held until the outcome is known.\n * Data-only (per CLAUDE.md: classes for data, explicit construction).\n */\nexport class GuardInvocation {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n public readonly root: string,\n public readonly timestamp: string,\n public readonly tool: string,\n public readonly target: string,\n public readonly branch: string,\n public readonly sync: string,\n public readonly projectDir: string,\n ) {}\n}\n\n/**\n * The per-INVOCATION stream — `.webpieces/logs/calls/<writer>.log` (see LogStream for the writer\n * key), one line for EVERY guards-hook\n * call (allow or block, bash or file), unlike `L2-decisions/` which records only the calls a\n * rule actually judged. It captures the tool, the command/file, the live git branch, the async-written\n * main-sync-status.json snapshot (branch / merged / fork-point / conflict), and — since this class\n * replaced a bare log-and-forget function — HOW THE CALL ENDED.\n *\n * WHY IT IS TWO CALLS. The line used to be written the moment the hook started, so it could not carry\n * a verdict: the decision had not been made yet. Answering \"what happened to this call?\" therefore\n * meant joining this file against the L2 decision stream BY TIMESTAMP, which is exactly the kind of\n * reconstruction a log exists to make unnecessary. So {@link begin} now only CAPTURES (including the\n * git/cache reads, which must still happen while the hook is running), and {@link finish} — called\n * from the hook's single terminal boundary, emitAllow/emitDeny — writes the whole line once the\n * outcome is known. The two streams stay distinct in purpose: this one is \"every call and how it\n * ended\", the decision log remains \"every judgement and why\".\n *\n * Every error is swallowed: logging must never block or fail a hook.\n */\nexport class InvocationLog {\n private pending: GuardInvocation | null = null;\n\n /**\n * Capture the context of one invocation. `cwd` is the AI's working dir; the repo root that owns\n * `.webpieces` is resolved from it. Writes NOTHING — {@link finish} does that.\n */\n begin(cwd: string, tool: string, target: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n const branch = branchForLog(root);\n // The cache is branch-keyed, so the entry to log is the one for the branch we are standing\n // on. 'unknown' (branchForLog's failure value) simply misses and logs 'sync=none'.\n const sync = summarizeSyncStatus(readMainSyncStatus(root, branch));\n this.pending = new GuardInvocation(root, new Date().toISOString(), tool, oneLine(target), branch, sync, claudeEnv.projectDirForLog());\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n\n /**\n * Write the captured line, now stamped with the outcome. A no-op when nothing was captured (the\n * 'rules' hook, or a terminal boundary reached before begin()), and it clears the pending entry so\n * a second emit cannot double-log.\n *\n * `rule` is the rule that blocked, or '-' when there is none; `fault` is the L0 fault code when this\n * call ended on one (S/C/Y — the JS-side faults), else '-'. FIELD ORDER IS APPEND-ONLY: the five\n * original fields keep their positions (cleanup automation mines this file), and `guards=` /\n * `rule=` / … / `fault=` are added at the end.\n */\n finish(verdict: Verdict, rule: string, fault: string = L0_FAULT_NONE): void {\n const invocation = this.pending;\n this.pending = null;\n if (invocation === null) return;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const logsDir = dotWebpieces.logsFile(invocation.root, CALLS_STREAM);\n fs.mkdirSync(logsDir, { recursive: true });\n const logPath = path.join(logsDir, logStream.writerFile('.log'));\n rotateLogFile(logPath, path.join(logsDir, logStream.writerFile('.1.log')));\n\n const line = [\n `[${invocation.timestamp}]`,\n invocation.tool,\n invocation.target,\n `branch=${invocation.branch}`,\n invocation.sync,\n // `guards=`, NOT `verdict=`. This hook can only report on ITSELF. Claude Code runs all\n // its PreToolUse hooks IN PARALLEL, so another hook process may deny a call this one\n // had no objection to, and neither can see the other's answer. Measured under the\n // RETIRED three-hook form: `cd <repo>/packages && ls` was DENIED by L-1 and recorded\n // here three times as `verdict=ALLOW`. The old field name promised an outcome it\n // structurally cannot know, so the name stayed even though L-1 is gone: the two\n // surviving hooks still run in parallel and still cannot see each other.\n `guards=${verdict}`,\n `rule=${oneLine(rule) || '-'}`,\n // The tree the guard ACTED in, next to what Claude Code said the project was. Both, on\n // every line, because the diagnostic value is entirely in comparing them — see\n // ClaudeEnv for the open question this field exists to settle empirically.\n `root=${invocation.root}`,\n `projectDir=${invocation.projectDir}`,\n // See logGuardDecision: the short tree label, so `tree=primary` with a matching\n // projectDir reads as healthy at a glance and `tree=<worktree>` beside a projectDir\n // pointing at the primary is the straddle, without diffing two absolute paths.\n `tree=${dotWebpieces.worktreeName(invocation.root) || 'primary'}`,\n // WHICH L0 fault ended this call, in the same letters and the same field name the L0 sh\n // shim uses (the `L0-shim/` stream) — so ONE grep spans the whole trail.\n `fault=${fault}`,\n ].join('\\t') + '\\n';\n fs.appendFileSync(logPath, line);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n }\n}\n\n// Process-wide instance: one hook process handles exactly one tool call, so a single pending entry is\n// the whole state there is. Module-scope (rather than DI) because the terminal boundary that flushes\n// it — emitAllow/emitDeny — is itself module-scope protocol code with no container in reach.\nexport const invocationLog = new InvocationLog();\n\n// One-field summary of main-sync-status.json for the invocation log: the branch the cache is FOR,\n// whether it is already merged (and its PR), fork-point presence, and conflict state — the signals a\n// cleanup step keys off. 'sync=none' when the cache has not been written yet (first call of a session).\nfunction summarizeSyncStatus(status: MainSyncStatus | null): string {\n if (status === null) return 'sync=none';\n const merged = status.branchAlreadyMerged ? `PR#${status.mergedPr !== '' ? status.mergedPr : '?'}` : 'no';\n return `sync=${status.branch} merged=${merged} fork=${String(status.hasForkPoint)} conflict=${String(status.conflict)} ts=${status.timestamp}`;\n}\n\n// Best-effort current branch for the log line. Returns 'unknown' on any failure (e.g. not a git\n// repo) — this is for display only, never for a control decision.\nexport function branchForLog(root: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: root,\n encoding: 'utf8',\n stdio: ['pipe', 'pipe', 'pipe'],\n }).trim() || 'unknown';\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return 'unknown';\n }\n}\n\n// Collapse newlines/tabs and cap length so one decision is always one log line.\nfunction oneLine(value: string): string {\n const flat = value.replace(/[\\t\\r\\n]+/g, ' ').trim();\n return flat.length <= MAX_TARGET_LEN ? flat : flat.slice(0, MAX_TARGET_LEN) + '…';\n}\n\nfunction rotateLogFile(logPath: string, prevPath: string): void {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const stat = fs.statSync(logPath);\n if (stat.size > MAX_LOG_BYTES) {\n if (fs.existsSync(prevPath)) fs.unlinkSync(prevPath);\n fs.renameSync(logPath, prevPath);\n }\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n }\n}\n"]}
|
|
@@ -31,3 +31,49 @@ export declare const L0_SH_FAULT_CODES: readonly ["D", "X", "U", "K"];
|
|
|
31
31
|
* anywhere identifying L0.
|
|
32
32
|
*/
|
|
33
33
|
export declare const L0_JS_FAULT_CODES: readonly ["S", "C", "Y"];
|
|
34
|
+
/**
|
|
35
|
+
* EVERY L0 fault code, as a type. The two arrays above are the halves; this is their union, and it is
|
|
36
|
+
* what makes `L0_FAULT_NAMES` TOTAL — a `Record<string, …>` would have forced a `?? 'unknown'` fallback
|
|
37
|
+
* at every read, which is shim shape #4 (a runtime default standing in for a type that could have
|
|
38
|
+
* expressed the invariant). With the union, a new fault added without a name is a COMPILE error, and
|
|
39
|
+
* neither reader needs a defensive branch.
|
|
40
|
+
*/
|
|
41
|
+
export type L0FaultCode = typeof L0_SH_FAULT_CODES[number] | typeof L0_JS_FAULT_CODES[number];
|
|
42
|
+
/**
|
|
43
|
+
* The stable, human-readable GUARD NAME per fault code — what goes in the deny's `[…]` header, in the
|
|
44
|
+
* matrix doc's own `guard` column, and nowhere else in a second spelling.
|
|
45
|
+
*
|
|
46
|
+
* L1 prints `[stale-main-bash-guard] (1 violation)` and L0 printed nothing comparable; the names below
|
|
47
|
+
* are deliberately in that same kebab shape so the two layers read as one system. They are IDENTITY, not
|
|
48
|
+
* prose: renaming one silently breaks a grep that spans all three artifacts, exactly as renumbering an
|
|
49
|
+
* L1 row would (see L1_ROWS' header).
|
|
50
|
+
*/
|
|
51
|
+
export declare const L0_FAULT_NAMES: Readonly<Record<L0FaultCode, string>>;
|
|
52
|
+
/**
|
|
53
|
+
* The three rows of L0's decision matrix, by number — the numbers `renderGuardMatrixDoc` prints, the
|
|
54
|
+
* numbers the audit line's `row=` carries, and the numbers a deny cites. L0's matrix has no genuine
|
|
55
|
+
* second dimension: every branch reduces to `fault present?` x `on the allowlist?`.
|
|
56
|
+
*
|
|
57
|
+
* All three are named because all three are LOGGED: the sh half writes a line for the healthy hand-down
|
|
58
|
+
* too (`PASS-BIN-ALLOW`), which is the line that tells "the guard ran and found nothing" apart from
|
|
59
|
+
* "the guard never ran".
|
|
60
|
+
*/
|
|
61
|
+
export declare const L0_ROW_HANDED_DOWN = "1";
|
|
62
|
+
/** Row 2: a fault is present but the call is on the L0 allowlist — a cure, a Read, or the config edit. */
|
|
63
|
+
export declare const L0_ROW_ALLOWLISTED = "2";
|
|
64
|
+
/** Row 3: a fault is present and the call is NOT on the allowlist. The one row that ever BLOCKS. */
|
|
65
|
+
export declare const L0_ROW_BLOCKED = "3";
|
|
66
|
+
/**
|
|
67
|
+
* The `[guard-name] (layer=L0 fault=<code> row=<n>)` header every L0 deny opens with, after the ❌ line.
|
|
68
|
+
*
|
|
69
|
+
* ONE builder, called by the JS denies directly and interpolated into the POSIX-sh denies at render
|
|
70
|
+
* time by renderShim() — so the sh half cannot spell the coordinates differently from the JS half even
|
|
71
|
+
* though it cannot import anything at runtime. `detail` is the per-fault count that mirrors
|
|
72
|
+
* formatReport's `(N violations)`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function l0GuardHeader(fault: L0FaultCode, detail: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* The one-line citation of WHICH matrix row was taken and on what dimension values — L1's pattern
|
|
77
|
+
* (its deny cites "`w` / `n` / `n` - row 8"), in L0's own two columns.
|
|
78
|
+
*/
|
|
79
|
+
export declare function l0MatrixCitation(fault: L0FaultCode): string;
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
// rule engine.
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
-
exports.L0_JS_FAULT_CODES = exports.L0_SH_FAULT_CODES = exports.L0_FAULT_NONE = exports.L0_FAULT_CONFIG_OUT_OF_SYNC = exports.L0_FAULT_CONFIG_MISSING = exports.L0_FAULT_SHIM_STALE = exports.L0_FAULT_BIN_BROKEN = exports.L0_FAULT_UNDECLARED = exports.L0_FAULT_BIN_MISSING = exports.L0_FAULT_DRIFT = void 0;
|
|
23
|
+
exports.L0_ROW_BLOCKED = exports.L0_ROW_ALLOWLISTED = exports.L0_ROW_HANDED_DOWN = exports.L0_FAULT_NAMES = exports.L0_JS_FAULT_CODES = exports.L0_SH_FAULT_CODES = exports.L0_FAULT_NONE = exports.L0_FAULT_CONFIG_OUT_OF_SYNC = exports.L0_FAULT_CONFIG_MISSING = exports.L0_FAULT_SHIM_STALE = exports.L0_FAULT_BIN_BROKEN = exports.L0_FAULT_UNDECLARED = exports.L0_FAULT_BIN_MISSING = exports.L0_FAULT_DRIFT = void 0;
|
|
24
|
+
exports.l0GuardHeader = l0GuardHeader;
|
|
25
|
+
exports.l0MatrixCitation = l0MatrixCitation;
|
|
24
26
|
/** `D` — version drift: the root package.json pin != the installed version. Decided in `sh`. */
|
|
25
27
|
exports.L0_FAULT_DRIFT = 'D';
|
|
26
28
|
/** `X` — the guard bin is missing (fresh clone, new worktree, package removed). Decided in `sh`. */
|
|
@@ -58,4 +60,56 @@ exports.L0_SH_FAULT_CODES = [
|
|
|
58
60
|
exports.L0_JS_FAULT_CODES = [
|
|
59
61
|
exports.L0_FAULT_SHIM_STALE, exports.L0_FAULT_CONFIG_MISSING, exports.L0_FAULT_CONFIG_OUT_OF_SYNC,
|
|
60
62
|
];
|
|
63
|
+
/**
|
|
64
|
+
* The stable, human-readable GUARD NAME per fault code — what goes in the deny's `[…]` header, in the
|
|
65
|
+
* matrix doc's own `guard` column, and nowhere else in a second spelling.
|
|
66
|
+
*
|
|
67
|
+
* L1 prints `[stale-main-bash-guard] (1 violation)` and L0 printed nothing comparable; the names below
|
|
68
|
+
* are deliberately in that same kebab shape so the two layers read as one system. They are IDENTITY, not
|
|
69
|
+
* prose: renaming one silently breaks a grep that spans all three artifacts, exactly as renumbering an
|
|
70
|
+
* L1 row would (see L1_ROWS' header).
|
|
71
|
+
*/
|
|
72
|
+
exports.L0_FAULT_NAMES = {
|
|
73
|
+
[exports.L0_FAULT_DRIFT]: 'version-drift',
|
|
74
|
+
[exports.L0_FAULT_BIN_MISSING]: 'guard-bin-missing',
|
|
75
|
+
[exports.L0_FAULT_UNDECLARED]: 'guard-pkg-undeclared',
|
|
76
|
+
[exports.L0_FAULT_BIN_BROKEN]: 'guard-bin-crashed',
|
|
77
|
+
[exports.L0_FAULT_SHIM_STALE]: 'managed-hook-surface',
|
|
78
|
+
[exports.L0_FAULT_CONFIG_MISSING]: 'config-missing',
|
|
79
|
+
[exports.L0_FAULT_CONFIG_OUT_OF_SYNC]: 'config-out-of-sync',
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* The three rows of L0's decision matrix, by number — the numbers `renderGuardMatrixDoc` prints, the
|
|
83
|
+
* numbers the audit line's `row=` carries, and the numbers a deny cites. L0's matrix has no genuine
|
|
84
|
+
* second dimension: every branch reduces to `fault present?` x `on the allowlist?`.
|
|
85
|
+
*
|
|
86
|
+
* All three are named because all three are LOGGED: the sh half writes a line for the healthy hand-down
|
|
87
|
+
* too (`PASS-BIN-ALLOW`), which is the line that tells "the guard ran and found nothing" apart from
|
|
88
|
+
* "the guard never ran".
|
|
89
|
+
*/
|
|
90
|
+
exports.L0_ROW_HANDED_DOWN = '1';
|
|
91
|
+
/** Row 2: a fault is present but the call is on the L0 allowlist — a cure, a Read, or the config edit. */
|
|
92
|
+
exports.L0_ROW_ALLOWLISTED = '2';
|
|
93
|
+
/** Row 3: a fault is present and the call is NOT on the allowlist. The one row that ever BLOCKS. */
|
|
94
|
+
exports.L0_ROW_BLOCKED = '3';
|
|
95
|
+
/**
|
|
96
|
+
* The `[guard-name] (layer=L0 fault=<code> row=<n>)` header every L0 deny opens with, after the ❌ line.
|
|
97
|
+
*
|
|
98
|
+
* ONE builder, called by the JS denies directly and interpolated into the POSIX-sh denies at render
|
|
99
|
+
* time by renderShim() — so the sh half cannot spell the coordinates differently from the JS half even
|
|
100
|
+
* though it cannot import anything at runtime. `detail` is the per-fault count that mirrors
|
|
101
|
+
* formatReport's `(N violations)`.
|
|
102
|
+
*/
|
|
103
|
+
// webpieces-disable no-function-outside-class -- pure string builder over this leaf module's own constants; it must stay importable by the dependency-free shim renderer.
|
|
104
|
+
function l0GuardHeader(fault, detail) {
|
|
105
|
+
return `[${exports.L0_FAULT_NAMES[fault]}] (layer=L0 fault=${fault} row=${exports.L0_ROW_BLOCKED}, ${detail})`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The one-line citation of WHICH matrix row was taken and on what dimension values — L1's pattern
|
|
109
|
+
* (its deny cites "`w` / `n` / `n` - row 8"), in L0's own two columns.
|
|
110
|
+
*/
|
|
111
|
+
// webpieces-disable no-function-outside-class -- sibling of l0GuardHeader in this leaf codebook module.
|
|
112
|
+
function l0MatrixCitation(fault) {
|
|
113
|
+
return `matrix row ${exports.L0_ROW_BLOCKED}: fault=${fault} present / on the allowlist? no -> BLOCK. Those are the same coordinates the audit line carries (layer=L0 row=${exports.L0_ROW_BLOCKED} fault=${fault}) and the same row webpieces.guard-matrix.md prints.`;
|
|
114
|
+
}
|
|
61
115
|
//# sourceMappingURL=l0-fault-codes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"l0-fault-codes.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l0-fault-codes.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,gFAAgF;AAChF,EAAE;AACF,oGAAoG;AACpG,gGAAgG;AAChG,sGAAsG;AACtG,uGAAuG;AACvG,qGAAqG;AACrG,2DAA2D;AAC3D,EAAE;AACF,sGAAsG;AACtG,6FAA6F;AAC7F,uFAAuF;AACvF,EAAE;AACF,mGAAmG;AACnG,wGAAwG;AACxG,uGAAuG;AACvG,oGAAoG;AACpG,eAAe;AACf,8EAA8E;;;AAE9E,gGAAgG;AACnF,QAAA,cAAc,GAAG,GAAG,CAAC;AAElC,oGAAoG;AACvF,QAAA,oBAAoB,GAAG,GAAG,CAAC;AAExC,iGAAiG;AACpF,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,oFAAoF;AACvE,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,uGAAuG;AAC1F,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,2EAA2E;AAC9D,QAAA,uBAAuB,GAAG,GAAG,CAAC;AAE3C,yFAAyF;AAC5E,QAAA,2BAA2B,GAAG,GAAG,CAAC;AAE/C;;;;;GAKG;AACU,QAAA,aAAa,GAAG,GAAG,CAAC;AAEjC;;;GAGG;AACU,QAAA,iBAAiB,GAAG;IAC7B,sBAAc,EAAE,4BAAoB,EAAE,2BAAmB,EAAE,2BAAmB;CACxE,CAAC;AAEX;;;;;GAKG;AACU,QAAA,iBAAiB,GAAG;IAC7B,2BAAmB,EAAE,+BAAuB,EAAE,mCAA2B;CACnE,CAAC","sourcesContent":["// ---------------------------------------------------------------------------\n// THE L0 FAULT CODEBOOK — one letter per fault, declared HERE and nowhere else.\n//\n// Both halves of L0 stamp `fault=<code>` onto their audit lines: the POSIX `sh` shim writes D/X/U/K\n// (shim-audit-log.ts), and the guard bin writes S/C/Y in JS (decision-log.ts, via runner.ts and\n// hook-core.ts). The whole value of that field is that ONE grep — `grep 'fault=S'` — spans the entire\n// trail, and that the faults actually observed can be diffed against `L0_FAULTS`. Both properties hold\n// only while every emitter spells the letters the SAME way, and a hand-retyped 'S' in one emitter is\n// exactly the drift this module exists to make impossible.\n//\n// It used to be retyped: the shim assigned `WP_FAULT=X` as a literal, SHIM_LOG_FAULTS listed the four\n// sh-side letters again, and L0_FAULTS listed all seven a third time. Three spellings of one\n// vocabulary, held together by a unit test that could only notice AFTER they diverged.\n//\n// This module is a LEAF — no imports at all — on purpose. `l0-matrix.ts` (core) builds `L0_FAULTS`\n// from these constants and `shim-audit-log.ts` / `shim.ts` (bin) render them into the shim; parking the\n// constants in either of those two would make the other one a core↔bin import cycle. It also keeps the\n// shim renderer dependency-light, which it must be: it has to work on a tree too broken to load the\n// rule engine.\n// ---------------------------------------------------------------------------\n\n/** `D` — version drift: the root package.json pin != the installed version. Decided in `sh`. */\nexport const L0_FAULT_DRIFT = 'D';\n\n/** `X` — the guard bin is missing (fresh clone, new worktree, package removed). Decided in `sh`. */\nexport const L0_FAULT_BIN_MISSING = 'X';\n\n/** `U` — the bin is missing AND nothing declares the package, so an install is a no-op. `sh`. */\nexport const L0_FAULT_UNDECLARED = 'U';\n\n/** `K` — the bin is present but CRASHED (corrupt node_modules). Decided in `sh`. */\nexport const L0_FAULT_BIN_BROKEN = 'K';\n\n/** `S` — the committed `.claude/webpieces/ai-hook.sh` != `renderShim()`. Decided in the bin, in JS. */\nexport const L0_FAULT_SHIM_STALE = 'S';\n\n/** `C` — `webpieces.config.json` is missing. Decided in the bin, in JS. */\nexport const L0_FAULT_CONFIG_MISSING = 'C';\n\n/** `Y` — a loaded rule has no `webpieces.config.json` key. Decided in the bin, in JS. */\nexport const L0_FAULT_CONFIG_OUT_OF_SYNC = 'Y';\n\n/**\n * No fault AT THIS LAYER — the value every audit line carries when nothing fired.\n *\n * Never a claim that nothing was wrong: a `fault=-` line from the `sh` shim only says the sh half found\n * nothing, and the bin it then exec'd may still have blocked on S/C/Y and stamped its own line.\n */\nexport const L0_FAULT_NONE = '-';\n\n/**\n * The faults decided in POSIX `sh`, BEFORE the bin runs — a stale, missing or broken validator cannot\n * be trusted to validate itself. In first-match-wins order.\n */\nexport const L0_SH_FAULT_CODES = [\n L0_FAULT_DRIFT, L0_FAULT_BIN_MISSING, L0_FAULT_UNDECLARED, L0_FAULT_BIN_BROKEN,\n] as const;\n\n/**\n * The faults decided INSIDE the guard bin, in JS. These reached the audit trail with no fault label at\n * all until the JS emitters started stamping them: an `S` storm that blocked an agent for ~20 tool\n * calls left two lines in the `rejections/` stream, both attributed to a downstream rule, and nothing\n * anywhere identifying L0.\n */\nexport const L0_JS_FAULT_CODES = [\n L0_FAULT_SHIM_STALE, L0_FAULT_CONFIG_MISSING, L0_FAULT_CONFIG_OUT_OF_SYNC,\n] as const;\n"]}
|
|
1
|
+
{"version":3,"file":"l0-fault-codes.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/l0-fault-codes.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,gFAAgF;AAChF,EAAE;AACF,oGAAoG;AACpG,gGAAgG;AAChG,sGAAsG;AACtG,uGAAuG;AACvG,qGAAqG;AACrG,2DAA2D;AAC3D,EAAE;AACF,sGAAsG;AACtG,6FAA6F;AAC7F,uFAAuF;AACvF,EAAE;AACF,mGAAmG;AACnG,wGAAwG;AACxG,uGAAuG;AACvG,oGAAoG;AACpG,eAAe;AACf,8EAA8E;;;AAyH9E,sCAEC;AAOD,4CAEC;AAlID,gGAAgG;AACnF,QAAA,cAAc,GAAG,GAAG,CAAC;AAElC,oGAAoG;AACvF,QAAA,oBAAoB,GAAG,GAAG,CAAC;AAExC,iGAAiG;AACpF,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,oFAAoF;AACvE,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,uGAAuG;AAC1F,QAAA,mBAAmB,GAAG,GAAG,CAAC;AAEvC,2EAA2E;AAC9D,QAAA,uBAAuB,GAAG,GAAG,CAAC;AAE3C,yFAAyF;AAC5E,QAAA,2BAA2B,GAAG,GAAG,CAAC;AAE/C;;;;;GAKG;AACU,QAAA,aAAa,GAAG,GAAG,CAAC;AAEjC;;;GAGG;AACU,QAAA,iBAAiB,GAAG;IAC7B,sBAAc,EAAE,4BAAoB,EAAE,2BAAmB,EAAE,2BAAmB;CACxE,CAAC;AAEX;;;;;GAKG;AACU,QAAA,iBAAiB,GAAG;IAC7B,2BAAmB,EAAE,+BAAuB,EAAE,mCAA2B;CACnE,CAAC;AA6BX;;;;;;;;GAQG;AACU,QAAA,cAAc,GAA0C;IACjE,CAAC,sBAAc,CAAC,EAAE,eAAe;IACjC,CAAC,4BAAoB,CAAC,EAAE,mBAAmB;IAC3C,CAAC,2BAAmB,CAAC,EAAE,sBAAsB;IAC7C,CAAC,2BAAmB,CAAC,EAAE,mBAAmB;IAC1C,CAAC,2BAAmB,CAAC,EAAE,sBAAsB;IAC7C,CAAC,+BAAuB,CAAC,EAAE,gBAAgB;IAC3C,CAAC,mCAA2B,CAAC,EAAE,oBAAoB;CACtD,CAAC;AAEF;;;;;;;;GAQG;AACU,QAAA,kBAAkB,GAAG,GAAG,CAAC;AAEtC,0GAA0G;AAC7F,QAAA,kBAAkB,GAAG,GAAG,CAAC;AAEtC,oGAAoG;AACvF,QAAA,cAAc,GAAG,GAAG,CAAC;AAElC;;;;;;;GAOG;AACH,0KAA0K;AAC1K,SAAgB,aAAa,CAAC,KAAkB,EAAE,MAAc;IAC5D,OAAO,IAAI,sBAAc,CAAC,KAAK,CAAC,qBAAqB,KAAK,QAAQ,sBAAc,KAAK,MAAM,GAAG,CAAC;AACnG,CAAC;AAED;;;GAGG;AACH,wGAAwG;AACxG,SAAgB,gBAAgB,CAAC,KAAkB;IAC/C,OAAO,cAAc,sBAAc,WAAW,KAAK,iHAAiH,sBAAc,UAAU,KAAK,sDAAsD,CAAC;AAC5P,CAAC","sourcesContent":["// ---------------------------------------------------------------------------\n// THE L0 FAULT CODEBOOK — one letter per fault, declared HERE and nowhere else.\n//\n// Both halves of L0 stamp `fault=<code>` onto their audit lines: the POSIX `sh` shim writes D/X/U/K\n// (shim-audit-log.ts), and the guard bin writes S/C/Y in JS (decision-log.ts, via runner.ts and\n// hook-core.ts). The whole value of that field is that ONE grep — `grep 'fault=S'` — spans the entire\n// trail, and that the faults actually observed can be diffed against `L0_FAULTS`. Both properties hold\n// only while every emitter spells the letters the SAME way, and a hand-retyped 'S' in one emitter is\n// exactly the drift this module exists to make impossible.\n//\n// It used to be retyped: the shim assigned `WP_FAULT=X` as a literal, SHIM_LOG_FAULTS listed the four\n// sh-side letters again, and L0_FAULTS listed all seven a third time. Three spellings of one\n// vocabulary, held together by a unit test that could only notice AFTER they diverged.\n//\n// This module is a LEAF — no imports at all — on purpose. `l0-matrix.ts` (core) builds `L0_FAULTS`\n// from these constants and `shim-audit-log.ts` / `shim.ts` (bin) render them into the shim; parking the\n// constants in either of those two would make the other one a core↔bin import cycle. It also keeps the\n// shim renderer dependency-light, which it must be: it has to work on a tree too broken to load the\n// rule engine.\n// ---------------------------------------------------------------------------\n\n/** `D` — version drift: the root package.json pin != the installed version. Decided in `sh`. */\nexport const L0_FAULT_DRIFT = 'D';\n\n/** `X` — the guard bin is missing (fresh clone, new worktree, package removed). Decided in `sh`. */\nexport const L0_FAULT_BIN_MISSING = 'X';\n\n/** `U` — the bin is missing AND nothing declares the package, so an install is a no-op. `sh`. */\nexport const L0_FAULT_UNDECLARED = 'U';\n\n/** `K` — the bin is present but CRASHED (corrupt node_modules). Decided in `sh`. */\nexport const L0_FAULT_BIN_BROKEN = 'K';\n\n/** `S` — the committed `.claude/webpieces/ai-hook.sh` != `renderShim()`. Decided in the bin, in JS. */\nexport const L0_FAULT_SHIM_STALE = 'S';\n\n/** `C` — `webpieces.config.json` is missing. Decided in the bin, in JS. */\nexport const L0_FAULT_CONFIG_MISSING = 'C';\n\n/** `Y` — a loaded rule has no `webpieces.config.json` key. Decided in the bin, in JS. */\nexport const L0_FAULT_CONFIG_OUT_OF_SYNC = 'Y';\n\n/**\n * No fault AT THIS LAYER — the value every audit line carries when nothing fired.\n *\n * Never a claim that nothing was wrong: a `fault=-` line from the `sh` shim only says the sh half found\n * nothing, and the bin it then exec'd may still have blocked on S/C/Y and stamped its own line.\n */\nexport const L0_FAULT_NONE = '-';\n\n/**\n * The faults decided in POSIX `sh`, BEFORE the bin runs — a stale, missing or broken validator cannot\n * be trusted to validate itself. In first-match-wins order.\n */\nexport const L0_SH_FAULT_CODES = [\n L0_FAULT_DRIFT, L0_FAULT_BIN_MISSING, L0_FAULT_UNDECLARED, L0_FAULT_BIN_BROKEN,\n] as const;\n\n/**\n * The faults decided INSIDE the guard bin, in JS. These reached the audit trail with no fault label at\n * all until the JS emitters started stamping them: an `S` storm that blocked an agent for ~20 tool\n * calls left two lines in the `rejections/` stream, both attributed to a downstream rule, and nothing\n * anywhere identifying L0.\n */\nexport const L0_JS_FAULT_CODES = [\n L0_FAULT_SHIM_STALE, L0_FAULT_CONFIG_MISSING, L0_FAULT_CONFIG_OUT_OF_SYNC,\n] as const;\n\n// ---------------------------------------------------------------------------\n// THE JOIN KEYS — the three artifacts that describe one L0 event, and the coordinates that line them up.\n//\n// There are three, and until now they could not be grepped together:\n// 1. THE DENY the agent reads in the moment (shim-deny-reason.ts for S; DENY_REASON_SH for D/X/U/K)\n// 2. THE AUDIT LINE (`.webpieces/logs/**`), which carries `layer=` `row=` `fault=`\n// 3. THE MATRIX DOC (webpieces.guard-matrix.md, rendered from L0_FAULTS + L0_ALLOWLIST)\n//\n// The deny had NONE of them: no fault letter, no row, and — the highest-value omission — no guard NAME,\n// while every L1/L2 deny opens `[<rule-name>] (N violations)`. So a transcript could not be debugged\n// against the log after the fact, and a reader could not find the matrix row by eye.\n//\n// These constants are that vocabulary, declared HERE for the same reason the letters are: this module is\n// a LEAF with no imports, so `core/l0-matrix.ts` (the doc), `bin/shim*.ts` (the denies) and\n// `core/decision-log.ts` (the log) can all reach it without an import cycle. Retyping a name in any one\n// of them is the drift this file exists to make impossible.\n// ---------------------------------------------------------------------------\n\n/**\n * EVERY L0 fault code, as a type. The two arrays above are the halves; this is their union, and it is\n * what makes `L0_FAULT_NAMES` TOTAL — a `Record<string, …>` would have forced a `?? 'unknown'` fallback\n * at every read, which is shim shape #4 (a runtime default standing in for a type that could have\n * expressed the invariant). With the union, a new fault added without a name is a COMPILE error, and\n * neither reader needs a defensive branch.\n */\nexport type L0FaultCode = typeof L0_SH_FAULT_CODES[number] | typeof L0_JS_FAULT_CODES[number];\n\n/**\n * The stable, human-readable GUARD NAME per fault code — what goes in the deny's `[…]` header, in the\n * matrix doc's own `guard` column, and nowhere else in a second spelling.\n *\n * L1 prints `[stale-main-bash-guard] (1 violation)` and L0 printed nothing comparable; the names below\n * are deliberately in that same kebab shape so the two layers read as one system. They are IDENTITY, not\n * prose: renaming one silently breaks a grep that spans all three artifacts, exactly as renumbering an\n * L1 row would (see L1_ROWS' header).\n */\nexport const L0_FAULT_NAMES: Readonly<Record<L0FaultCode, string>> = {\n [L0_FAULT_DRIFT]: 'version-drift',\n [L0_FAULT_BIN_MISSING]: 'guard-bin-missing',\n [L0_FAULT_UNDECLARED]: 'guard-pkg-undeclared',\n [L0_FAULT_BIN_BROKEN]: 'guard-bin-crashed',\n [L0_FAULT_SHIM_STALE]: 'managed-hook-surface',\n [L0_FAULT_CONFIG_MISSING]: 'config-missing',\n [L0_FAULT_CONFIG_OUT_OF_SYNC]: 'config-out-of-sync',\n};\n\n/**\n * The three rows of L0's decision matrix, by number — the numbers `renderGuardMatrixDoc` prints, the\n * numbers the audit line's `row=` carries, and the numbers a deny cites. L0's matrix has no genuine\n * second dimension: every branch reduces to `fault present?` x `on the allowlist?`.\n *\n * All three are named because all three are LOGGED: the sh half writes a line for the healthy hand-down\n * too (`PASS-BIN-ALLOW`), which is the line that tells \"the guard ran and found nothing\" apart from\n * \"the guard never ran\".\n */\nexport const L0_ROW_HANDED_DOWN = '1';\n\n/** Row 2: a fault is present but the call is on the L0 allowlist — a cure, a Read, or the config edit. */\nexport const L0_ROW_ALLOWLISTED = '2';\n\n/** Row 3: a fault is present and the call is NOT on the allowlist. The one row that ever BLOCKS. */\nexport const L0_ROW_BLOCKED = '3';\n\n/**\n * The `[guard-name] (layer=L0 fault=<code> row=<n>)` header every L0 deny opens with, after the ❌ line.\n *\n * ONE builder, called by the JS denies directly and interpolated into the POSIX-sh denies at render\n * time by renderShim() — so the sh half cannot spell the coordinates differently from the JS half even\n * though it cannot import anything at runtime. `detail` is the per-fault count that mirrors\n * formatReport's `(N violations)`.\n */\n// webpieces-disable no-function-outside-class -- pure string builder over this leaf module's own constants; it must stay importable by the dependency-free shim renderer.\nexport function l0GuardHeader(fault: L0FaultCode, detail: string): string {\n return `[${L0_FAULT_NAMES[fault]}] (layer=L0 fault=${fault} row=${L0_ROW_BLOCKED}, ${detail})`;\n}\n\n/**\n * The one-line citation of WHICH matrix row was taken and on what dimension values — L1's pattern\n * (its deny cites \"`w` / `n` / `n` - row 8\"), in L0's own two columns.\n */\n// webpieces-disable no-function-outside-class -- sibling of l0GuardHeader in this leaf codebook module.\nexport function l0MatrixCitation(fault: L0FaultCode): string {\n return `matrix row ${L0_ROW_BLOCKED}: fault=${fault} present / on the allowlist? no -> BLOCK. Those are the same coordinates the audit line carries (layer=L0 row=${L0_ROW_BLOCKED} fault=${fault}) and the same row webpieces.guard-matrix.md prints.`;\n}\n"]}
|
package/src/core/l0-matrix.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { L0Call } from '../bin/shim';
|
|
2
|
+
import { L0FaultCode } from './l0-fault-codes';
|
|
2
3
|
/**
|
|
3
4
|
* The doc L0's deny messages point at. Lives in @webpieces/rules-config/templates alongside the others,
|
|
4
5
|
* and is written to <root>/.webpieces/instruct-ai/ lazily, only on an L0 BLOCK.
|
|
@@ -47,7 +48,12 @@ export declare class L0Cure {
|
|
|
47
48
|
}
|
|
48
49
|
/** One L0 fault. Data-only → a class, per CLAUDE.md. */
|
|
49
50
|
export declare class L0Fault {
|
|
50
|
-
|
|
51
|
+
/**
|
|
52
|
+
* The codebook's letter — typed as the UNION of every declared code, not `string`, so
|
|
53
|
+
* `L0_FAULT_NAMES[code]` is total and neither this module nor the deny builders need a
|
|
54
|
+
* `?? 'unknown'` fallback. A fault added without a name fails to compile.
|
|
55
|
+
*/
|
|
56
|
+
readonly code: L0FaultCode;
|
|
51
57
|
readonly name: string;
|
|
52
58
|
readonly detectedBy: string;
|
|
53
59
|
readonly enforcedIn: string;
|
|
@@ -58,7 +64,13 @@ export declare class L0Fault {
|
|
|
58
64
|
* the same bytes the consumer runs, which is what the mention assertion needs to search.
|
|
59
65
|
*/
|
|
60
66
|
readonly denyText: string;
|
|
61
|
-
constructor(
|
|
67
|
+
constructor(
|
|
68
|
+
/**
|
|
69
|
+
* The codebook's letter — typed as the UNION of every declared code, not `string`, so
|
|
70
|
+
* `L0_FAULT_NAMES[code]` is total and neither this module nor the deny builders need a
|
|
71
|
+
* `?? 'unknown'` fallback. A fault added without a name fails to compile.
|
|
72
|
+
*/
|
|
73
|
+
code: L0FaultCode, name: string, detectedBy: string, enforcedIn: string, cures: readonly L0Cure[],
|
|
62
74
|
/**
|
|
63
75
|
* The artifact carrying this fault's deny text. For S/C/Y that is the deny string itself; for
|
|
64
76
|
* D/X/U/K the text is built in POSIX sh inside the rendered shim, so it is the rendered shim —
|
|
@@ -67,7 +79,7 @@ export declare class L0Fault {
|
|
|
67
79
|
denyText: string);
|
|
68
80
|
}
|
|
69
81
|
export declare const CONFIG_MISSING_REPORT: string;
|
|
70
|
-
export declare const CONFIG_OUT_OF_SYNC_HEADER
|
|
82
|
+
export declare const CONFIG_OUT_OF_SYNC_HEADER: string;
|
|
71
83
|
/**
|
|
72
84
|
* THE L0 faults, in first-match-wins order. D/X/U/K are decided in POSIX sh BEFORE the bin runs (a
|
|
73
85
|
* stale, missing or broken validator cannot be trusted to validate itself); S/C/Y are decided inside
|
|
@@ -91,5 +103,12 @@ export declare function renderGuardMatrixDoc(): string;
|
|
|
91
103
|
* with a crash.
|
|
92
104
|
*/
|
|
93
105
|
export declare function writeGuardMatrixDoc(workspaceRoot: string): string;
|
|
94
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* The `READ <path>` pointer appended to an L0 deny, or '' when the doc could not be written.
|
|
108
|
+
*
|
|
109
|
+
* It opens with a NEWLINE, not a space: the JS-side L0 denies render in the house format now (a header,
|
|
110
|
+
* a `[guard-name]` block, `Fix Option N:` lines), so a pointer glued onto the end of the last line would
|
|
111
|
+
* be the one place the shape broke. A real newline is safe on both call paths — denyJson() JSON.stringifies
|
|
112
|
+
* it, exactly as it does for every multi-line L1 report.
|
|
113
|
+
*/
|
|
95
114
|
export declare function guardMatrixPointer(docPath: string): string;
|
package/src/core/l0-matrix.js
CHANGED
|
@@ -78,7 +78,13 @@ class L0Fault {
|
|
|
78
78
|
enforcedIn;
|
|
79
79
|
cures;
|
|
80
80
|
denyText;
|
|
81
|
-
constructor(
|
|
81
|
+
constructor(
|
|
82
|
+
/**
|
|
83
|
+
* The codebook's letter — typed as the UNION of every declared code, not `string`, so
|
|
84
|
+
* `L0_FAULT_NAMES[code]` is total and neither this module nor the deny builders need a
|
|
85
|
+
* `?? 'unknown'` fallback. A fault added without a name fails to compile.
|
|
86
|
+
*/
|
|
87
|
+
code, name, detectedBy, enforcedIn, cures,
|
|
82
88
|
/**
|
|
83
89
|
* The artifact carrying this fault's deny text. For S/C/Y that is the deny string itself; for
|
|
84
90
|
* D/X/U/K the text is built in POSIX sh inside the rendered shim, so it is the rendered shim —
|
|
@@ -108,19 +114,41 @@ exports.L0Fault = L0Fault;
|
|
|
108
114
|
// agent. Writing the file is the one cure that always works, and it is the same cure every other config
|
|
109
115
|
// problem has (see the config-validation invariant in guards/L0-tooling.md): the validator reports every
|
|
110
116
|
// error at once, so the write/validate loop converges in a couple of passes.
|
|
111
|
-
exports.CONFIG_MISSING_REPORT =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
'
|
|
117
|
-
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
exports.CONFIG_MISSING_REPORT = [
|
|
118
|
+
`❌ webpieces ai-hooks blocked this call: ${rules_config_1.CONFIG_FILENAME} not found.`,
|
|
119
|
+
'',
|
|
120
|
+
(0, l0_fault_codes_1.l0GuardHeader)(l0_fault_codes_1.L0_FAULT_CONFIG_MISSING, '1 violation'),
|
|
121
|
+
` ${rules_config_1.CONFIG_FILENAME}`,
|
|
122
|
+
' → the webpieces guards cannot run without it, so every OTHER tool call is blocked.',
|
|
123
|
+
` → ${(0, l0_fault_codes_1.l0MatrixCitation)(l0_fault_codes_1.L0_FAULT_CONFIG_MISSING)}`,
|
|
124
|
+
'',
|
|
125
|
+
'Still allowed while this block is up:',
|
|
126
|
+
' - any Read',
|
|
127
|
+
` - any Write/Edit whose target is ${rules_config_1.CONFIG_FILENAME}`,
|
|
128
|
+
' - every command on the L0 allowlist, including the Fix Options below',
|
|
129
|
+
' THIS IS NOT A DEADLOCK - run one YOURSELF now; do not hand it back to the human.',
|
|
130
|
+
'',
|
|
131
|
+
` Fix Option 1: (preferred) it needs no other tool and it never prompts - create ${rules_config_1.CONFIG_FILENAME}`,
|
|
132
|
+
' yourself. The validator reports EVERY missing/invalid entry at once (each with the snippet to',
|
|
133
|
+
' paste), so a minimal first draft converges in about two passes.',
|
|
134
|
+
' Fix Option 2: pick this ONLY at an interactive terminal where you can answer its two prompts - it',
|
|
135
|
+
' goes on to wire the Claude Code hooks and asks for a target twice, which hangs a non-interactive',
|
|
136
|
+
' session.',
|
|
137
|
+
' run EXACTLY: `pnpm exec wp-install-ai-hooks`',
|
|
138
|
+
'',
|
|
139
|
+
'Do not append anything to the option you pick — the allowlist is anchored to the whole command.',
|
|
140
|
+
].join('\n');
|
|
141
|
+
// The HEADER of the fault-Y deny (built out in runner.checkConfigSync, which appends the per-rule
|
|
142
|
+
// detail as the `[…]` block's offenders). Kept here so the fault table quotes the same text the runner
|
|
143
|
+
// emits, and so Y opens with the same `[guard-name] (layer=L0 fault=Y row=3)` coordinates as every
|
|
144
|
+
// other L0 fault — that triple is what joins the deny to the audit line and to the matrix row.
|
|
145
|
+
exports.CONFIG_OUT_OF_SYNC_HEADER = [
|
|
146
|
+
`❌ webpieces ai-hooks blocked this call: ${rules_config_1.CONFIG_FILENAME} is out of sync.`,
|
|
147
|
+
'',
|
|
148
|
+
(0, l0_fault_codes_1.l0GuardHeader)(l0_fault_codes_1.L0_FAULT_CONFIG_OUT_OF_SYNC, '1 violation'),
|
|
149
|
+
` new built-in rules are present that have no entry in ${rules_config_1.CONFIG_FILENAME}`,
|
|
150
|
+
` → ${(0, l0_fault_codes_1.l0MatrixCitation)(l0_fault_codes_1.L0_FAULT_CONFIG_OUT_OF_SYNC)}`,
|
|
151
|
+
].join('\n');
|
|
124
152
|
// Writing/repairing the file yourself. PREFERRED for both config faults, per the config-validation
|
|
125
153
|
// invariant in guards/L0-tooling.md: every config problem cures to "make the file right", the validator
|
|
126
154
|
// reports all errors at once so the loop converges in a couple of passes, and allowlist entry 2 permits
|
|
@@ -144,8 +172,13 @@ exports.L0_FAULTS = [
|
|
|
144
172
|
// you WANT, which is what the second cure is for.
|
|
145
173
|
bashCure('pnpm install', true, 'node_modules is OLDER than the pin, OR you are on a feature branch and want YOUR '
|
|
146
174
|
+ 'branch pin (usually the case) — it always clears the drift'),
|
|
147
|
-
|
|
148
|
-
|
|
175
|
+
// The on-main sync is spelled `git checkout main && git pull origin main` and NOT `git pull`:
|
|
176
|
+
// a raw pull on a FEATURE branch merges main into it and destroys the fork point, so it is
|
|
177
|
+
// no longer on the L0 allowlist at all (see CHECKOUT_MAIN_PULL_BODY_ERE). This spelling ends
|
|
178
|
+
// ON main, which is why it is safe from any branch — and it is a no-op checkout when you are
|
|
179
|
+
// already there.
|
|
180
|
+
bashCure(shim_1.CHECKOUT_MAIN_PULL_CMD, false, 'node_modules is NEWER than the pin AND you are on main — the PIN is the stale side, so '
|
|
181
|
+
+ 'sync first and install second; a bare install would downgrade you'),
|
|
149
182
|
], (0, shim_1.renderShim)()),
|
|
150
183
|
new L0Fault(l0_fault_codes_1.L0_FAULT_BIN_MISSING, 'guard bin missing (fresh clone / new worktree / package removed)', 'sh, before the bin runs', 'sh', [bashCure('pnpm install', true, 'this fault fires at all — nothing is installed in THIS tree, and a new git worktree '
|
|
151
184
|
+ 'copies no node_modules')], (0, shim_1.renderShim)()),
|
|
@@ -224,9 +257,15 @@ function renderGuardMatrixDoc() {
|
|
|
224
257
|
'',
|
|
225
258
|
'## The faults',
|
|
226
259
|
'',
|
|
227
|
-
'
|
|
228
|
-
'
|
|
229
|
-
|
|
260
|
+
'THE JOIN KEYS ARE `guard`, `fault=` and `row=`. Every L0 deny opens',
|
|
261
|
+
'`[<guard>] (layer=L0 fault=<code> row=3, …)`, and every audit line — from BOTH halves of L0, the',
|
|
262
|
+
'`sh` shim and the guard bin — carries `layer=L0 row=<n> fault=<code>`. So one grep lands you in',
|
|
263
|
+
'the deny, the log line and the row below. The guard names come from `L0_FAULT_NAMES` and the row',
|
|
264
|
+
'numbers from `L0_ROW_*`, both spelled in exactly one place (`core/l0-fault-codes.ts`).',
|
|
265
|
+
'',
|
|
266
|
+
'| code | guard | fault | detected by | enforced in |',
|
|
267
|
+
'|---|---|---|---|---|',
|
|
268
|
+
...exports.L0_FAULTS.map((f) => `| \`${f.code}\` | \`${l0_fault_codes_1.L0_FAULT_NAMES[f.code]}\` | ${f.name} | ${f.detectedBy} | ${f.enforcedIn} |`),
|
|
230
269
|
'',
|
|
231
270
|
'First match wins. `D`/`X`/`U`/`K` are decided in POSIX `sh` inside the committed shim, BEFORE the',
|
|
232
271
|
'guard bin runs — a stale, missing or broken validator cannot be trusted to validate itself.',
|
|
@@ -256,8 +295,12 @@ function renderMatrixAndAllowlist() {
|
|
|
256
295
|
'| # | fault | on the allowlist? | outcome |',
|
|
257
296
|
'|---|---|---|---|',
|
|
258
297
|
'| 1 | none | — | hand down to the next guard layer |',
|
|
259
|
-
|
|
260
|
-
|
|
298
|
+
`| ${l0_fault_codes_1.L0_ROW_ALLOWLISTED} | any | yes | PASS or ALLOW (see the entry) |`,
|
|
299
|
+
`| ${l0_fault_codes_1.L0_ROW_BLOCKED} | any | no | BLOCK — **only the message varies by fault** |`,
|
|
300
|
+
'',
|
|
301
|
+
`Row ${l0_fault_codes_1.L0_ROW_BLOCKED} is the only row that blocks, so every L0 deny cites it — \`row=${l0_fault_codes_1.L0_ROW_BLOCKED}\` in the`,
|
|
302
|
+
'deny header, `row=' + l0_fault_codes_1.L0_ROW_BLOCKED + '` on the audit line, and this row here. Same numbers as L1 uses for',
|
|
303
|
+
'its own rows (see `L1_ROWS`), and for the same reason: a row number is IDENTITY, so it is never reused.',
|
|
261
304
|
'',
|
|
262
305
|
'The tool is not a dimension either: "any Read" is an allowlist ENTRY, not a tool check.',
|
|
263
306
|
'',
|
|
@@ -265,7 +308,7 @@ function renderMatrixAndAllowlist() {
|
|
|
265
308
|
'',
|
|
266
309
|
'ONE list, consulted identically by every fault. A cure that cannot help a given fault also',
|
|
267
310
|
'cannot hurt it, and gating each entry on a fault is what produced four real defects (a stale',
|
|
268
|
-
'shim that denied `pnpm install` and
|
|
311
|
+
'shim that denied `pnpm install` and every git sync; faults that denied every Read; a config fault',
|
|
269
312
|
'that denied `rm -rf node_modules && pnpm install` while allowing a bare `pnpm install`).',
|
|
270
313
|
'',
|
|
271
314
|
'| # | allowed | outcome |',
|
|
@@ -316,11 +359,18 @@ function writeGuardMatrixDoc(workspaceRoot) {
|
|
|
316
359
|
return '';
|
|
317
360
|
}
|
|
318
361
|
}
|
|
319
|
-
/**
|
|
362
|
+
/**
|
|
363
|
+
* The `READ <path>` pointer appended to an L0 deny, or '' when the doc could not be written.
|
|
364
|
+
*
|
|
365
|
+
* It opens with a NEWLINE, not a space: the JS-side L0 denies render in the house format now (a header,
|
|
366
|
+
* a `[guard-name]` block, `Fix Option N:` lines), so a pointer glued onto the end of the last line would
|
|
367
|
+
* be the one place the shape broke. A real newline is safe on both call paths — denyJson() JSON.stringifies
|
|
368
|
+
* it, exactly as it does for every multi-line L1 report.
|
|
369
|
+
*/
|
|
320
370
|
// webpieces-disable no-function-outside-class -- sibling of writeGuardMatrixDoc in this module
|
|
321
371
|
function guardMatrixPointer(docPath) {
|
|
322
372
|
if (docPath === '')
|
|
323
373
|
return '';
|
|
324
|
-
return
|
|
374
|
+
return `\nThe full L0 guard matrix - every fault and everything that is allowed through - is at ${docPath}; READ it if you are unsure why this call was blocked.`;
|
|
325
375
|
}
|
|
326
376
|
//# sourceMappingURL=l0-matrix.js.map
|