@codyswann/lisa 4.84.6 → 4.84.8
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/all/copy-overwrite/scripts/lib/completion-authorization.mjs +282 -0
- package/all/copy-overwrite/scripts/lib/completion-proof-adapters.mjs +213 -0
- package/all/copy-overwrite/scripts/lib/completion-proof-conformance.mjs +115 -0
- package/all/copy-overwrite/scripts/lib/completion-proof-content.mjs +408 -0
- package/all/copy-overwrite/scripts/lib/completion-proof-errors.mjs +16 -0
- package/all/copy-overwrite/scripts/lib/completion-proof-github.mjs +233 -0
- package/all/copy-overwrite/scripts/lib/completion-proof-markdown.mjs +70 -0
- package/all/copy-overwrite/scripts/lib/completion-proof-projection.mjs +169 -0
- package/all/copy-overwrite/scripts/lib/completion-proof-publisher.mjs +238 -0
- package/all/copy-overwrite/scripts/lib/completion-proof-reader.mjs +175 -0
- package/all/copy-overwrite/scripts/lib/completion-proof-shape.mjs +374 -0
- package/all/copy-overwrite/scripts/lib/completion-proof-source.mjs +174 -0
- package/all/copy-overwrite/scripts/lib/history-secret-scanner.mjs +20 -5
- package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +15 -0
- package/all/copy-overwrite/scripts/lib/usage-effectiveness.d.mts +27 -0
- package/all/copy-overwrite/scripts/lib/usage-effectiveness.mjs +156 -0
- package/all/copy-overwrite/scripts/lib/usage-ledger-integrity.d.mts +8 -0
- package/all/copy-overwrite/scripts/lib/usage-ledger-integrity.mjs +141 -0
- package/all/copy-overwrite/scripts/lib/usage-ledger.d.mts +89 -0
- package/all/copy-overwrite/scripts/lib/usage-ledger.mjs +206 -0
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +3 -1
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +255 -39
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +141 -3
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +186 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +108 -15
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +3 -1
- package/dist/utils/effectiveness.d.ts +4 -41
- package/dist/utils/effectiveness.d.ts.map +1 -1
- package/dist/utils/effectiveness.js +2 -126
- package/dist/utils/effectiveness.js.map +1 -1
- package/dist/utils/usage-accounting.d.ts +5 -100
- package/dist/utils/usage-accounting.d.ts.map +1 -1
- package/dist/utils/usage-accounting.js +4 -287
- package/dist/utils/usage-accounting.js.map +1 -1
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +15 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-tracker-evidence/SKILL.md +7 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-tracker-evidence/references/completion-authorization.md +193 -0
- package/plugins/lisa/hooks/block-host-name-leak.mjs +25 -13
- package/plugins/lisa/hooks/host-name-command-context.mjs +16 -4
- package/plugins/lisa/hooks/parity-safety-net.sh +3 -1
- package/plugins/lisa/rules/eager/00-rule-index.md +1 -0
- package/plugins/lisa/rules/reference/completion-authorization.md +10 -0
- package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +15 -1
- package/plugins/lisa/skills/lisa-tracker-evidence/SKILL.md +7 -1
- package/plugins/lisa/skills/lisa-tracker-evidence/references/completion-authorization.md +193 -0
- package/plugins/lisa-agy/hooks/block-host-name-leak.mjs +25 -13
- package/plugins/lisa-agy/hooks/host-name-command-context.mjs +16 -4
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +3 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +15 -1
- package/plugins/lisa-agy/skills/lisa-tracker-evidence/SKILL.md +7 -1
- package/plugins/lisa-agy/skills/lisa-tracker-evidence/references/completion-authorization.md +193 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/block-host-name-leak.mjs +25 -13
- package/plugins/lisa-copilot/hooks/host-name-command-context.mjs +16 -4
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +3 -1
- package/plugins/lisa-copilot/rules/eager/00-rule-index.md +1 -0
- package/plugins/lisa-copilot/rules/reference/completion-authorization.md +10 -0
- package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +15 -1
- package/plugins/lisa-copilot/skills/lisa-tracker-evidence/SKILL.md +7 -1
- package/plugins/lisa-copilot/skills/lisa-tracker-evidence/references/completion-authorization.md +193 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-host-name-leak.mjs +25 -13
- package/plugins/lisa-cursor/hooks/host-name-command-context.mjs +16 -4
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +3 -1
- package/plugins/lisa-cursor/rules/00-rule-index.mdc +1 -0
- package/plugins/lisa-cursor/rules/completion-authorization-reference.mdc +15 -0
- package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +15 -1
- package/plugins/lisa-cursor/skills/lisa-tracker-evidence/SKILL.md +7 -1
- package/plugins/lisa-cursor/skills/lisa-tracker-evidence/references/completion-authorization.md +193 -0
- package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/hooks/block-host-name-leak.mjs +25 -13
- package/plugins/lisa-devin/hooks/host-name-command-context.mjs +16 -4
- package/plugins/lisa-devin/hooks/parity-safety-net.sh +3 -1
- package/plugins/lisa-devin/rules/eager/00-rule-index.md +1 -0
- package/plugins/lisa-devin/rules/reference/completion-authorization.md +10 -0
- package/plugins/lisa-devin/skills/lisa-drive-pr-to-merge/SKILL.md +15 -1
- package/plugins/lisa-devin/skills/lisa-tracker-evidence/SKILL.md +7 -1
- package/plugins/lisa-devin/skills/lisa-tracker-evidence/references/completion-authorization.md +193 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/block-host-name-leak.mjs +25 -13
- package/plugins/src/base/hooks/host-name-command-context.mjs +16 -4
- package/plugins/src/base/hooks/parity-safety-net.sh +3 -1
- package/plugins/src/base/rules/eager/00-rule-index.md +1 -0
- package/plugins/src/base/rules/reference/completion-authorization.md +10 -0
- package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +15 -1
- package/plugins/src/base/skills/lisa-tracker-evidence/SKILL.md +7 -1
- package/plugins/src/base/skills/lisa-tracker-evidence/references/completion-authorization.md +193 -0
- package/scripts/lib/host-name-expanded-command-cases.mjs +286 -0
- package/scripts/lib/host-name-field-occurrence-cases.mjs +60 -0
- package/scripts/two-channel-couplings.json +2 -2
- package/scripts/verify-host-name-target-scope.mjs +27 -1
- package/typescript/copy-overwrite/.prettierignore +15 -0
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +32 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
const FIELDS = [
|
|
5
|
+
"schema",
|
|
6
|
+
"lisaVersion",
|
|
7
|
+
"workerConfigRevision",
|
|
8
|
+
"workerWallMs",
|
|
9
|
+
"feedbackMs",
|
|
10
|
+
"workerSource",
|
|
11
|
+
"attention",
|
|
12
|
+
"readyAt",
|
|
13
|
+
"acceptedAt",
|
|
14
|
+
"lifecycleSources",
|
|
15
|
+
"reworkSources",
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Require a nonempty evidence identifier.
|
|
19
|
+
* @param value - Untrusted field
|
|
20
|
+
* @returns Whether this is a nonempty string
|
|
21
|
+
*/
|
|
22
|
+
export function isEvidenceString(value) {
|
|
23
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Require an unambiguous UTC instant rather than a locale-dependent date.
|
|
27
|
+
* @param value - Untrusted timestamp
|
|
28
|
+
* @returns Whether this is a canonical UTC instant
|
|
29
|
+
*/
|
|
30
|
+
export function isEvidenceTime(value) {
|
|
31
|
+
return (
|
|
32
|
+
typeof value === "string" &&
|
|
33
|
+
Number.isFinite(Date.parse(value)) &&
|
|
34
|
+
new Date(value).toISOString() === value
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Validate a duration without converting missing values to zero.
|
|
39
|
+
* @param value - Untrusted duration
|
|
40
|
+
* @returns Whether duration is finite and nonnegative
|
|
41
|
+
*/
|
|
42
|
+
function isDuration(value) {
|
|
43
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate optional source lists.
|
|
47
|
+
* @param value - Untrusted sources
|
|
48
|
+
* @returns Whether sources are valid or explicitly unavailable
|
|
49
|
+
*/
|
|
50
|
+
function isSources(value) {
|
|
51
|
+
return (
|
|
52
|
+
value === null || (Array.isArray(value) && value.every(isEvidenceString))
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Validate observed human interventions; timestamps cannot imply minutes of work.
|
|
57
|
+
* @param value - Untrusted events
|
|
58
|
+
* @returns Whether all events carry stable identity and source
|
|
59
|
+
*/
|
|
60
|
+
function isAttention(value) {
|
|
61
|
+
return (
|
|
62
|
+
value === null ||
|
|
63
|
+
(Array.isArray(value) &&
|
|
64
|
+
value.every(
|
|
65
|
+
event =>
|
|
66
|
+
event !== null &&
|
|
67
|
+
typeof event === "object" &&
|
|
68
|
+
Object.keys(event).length === 2 &&
|
|
69
|
+
isEvidenceString(event.id) &&
|
|
70
|
+
isEvidenceString(event.source)
|
|
71
|
+
))
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check nullable clock fields and the evidence needed to interpret them.
|
|
76
|
+
* @param row - Candidate observation
|
|
77
|
+
* @returns Whether clocks are valid and sourced
|
|
78
|
+
*/
|
|
79
|
+
function validClocks(row) {
|
|
80
|
+
const durations =
|
|
81
|
+
(row.workerWallMs === null || isDuration(row.workerWallMs)) &&
|
|
82
|
+
(row.feedbackMs === null ||
|
|
83
|
+
(Array.isArray(row.feedbackMs) && row.feedbackMs.every(isDuration)));
|
|
84
|
+
const sourcedWorker =
|
|
85
|
+
(row.workerWallMs === null && row.feedbackMs === null) ||
|
|
86
|
+
isEvidenceString(row.workerSource);
|
|
87
|
+
const times = [row.readyAt, row.acceptedAt].every(
|
|
88
|
+
value => value === null || isEvidenceTime(value)
|
|
89
|
+
);
|
|
90
|
+
const sourcedLifecycle =
|
|
91
|
+
(row.readyAt === null && row.acceptedAt === null) ||
|
|
92
|
+
(Array.isArray(row.lifecycleSources) && row.lifecycleSources.length > 0);
|
|
93
|
+
const ordered =
|
|
94
|
+
!isEvidenceTime(row.readyAt) ||
|
|
95
|
+
!isEvidenceTime(row.acceptedAt) ||
|
|
96
|
+
Date.parse(row.acceptedAt) >= Date.parse(row.readyAt);
|
|
97
|
+
return durations && sourcedWorker && times && sourcedLifecycle && ordered;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Parse additive observation data, refusing malformed measurements rather than guessing.
|
|
101
|
+
* @param value - JSON-compatible input
|
|
102
|
+
* @returns Validated observation
|
|
103
|
+
*/
|
|
104
|
+
export function parseEffectiveness(value) {
|
|
105
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
106
|
+
throw new Error("Invalid effectiveness observation");
|
|
107
|
+
const row = value;
|
|
108
|
+
const shape =
|
|
109
|
+
Object.keys(row).length === FIELDS.length &&
|
|
110
|
+
FIELDS.every(key => Object.hasOwn(row, key));
|
|
111
|
+
const tags = [
|
|
112
|
+
row.lisaVersion,
|
|
113
|
+
row.workerConfigRevision,
|
|
114
|
+
row.workerSource,
|
|
115
|
+
].every(tag => tag === null || isEvidenceString(tag));
|
|
116
|
+
if (
|
|
117
|
+
!shape ||
|
|
118
|
+
row.schema !== 1 ||
|
|
119
|
+
!tags ||
|
|
120
|
+
!isAttention(row.attention) ||
|
|
121
|
+
!isSources(row.lifecycleSources) ||
|
|
122
|
+
!isSources(row.reworkSources) ||
|
|
123
|
+
!validClocks(row)
|
|
124
|
+
)
|
|
125
|
+
throw new Error("Invalid or unsourced effectiveness observation");
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Read correlated extensions and reject ambiguous repeated values.
|
|
130
|
+
* @param section - Managed usage markdown
|
|
131
|
+
* @returns Observation keyed by usage entry identity
|
|
132
|
+
*/
|
|
133
|
+
export function parseEffectivenessTokens(section) {
|
|
134
|
+
const rows = Array.from(
|
|
135
|
+
section.matchAll(/<!-- lisa:usage-effectiveness [^\r\n]*? -->/gu),
|
|
136
|
+
token => {
|
|
137
|
+
const match =
|
|
138
|
+
/^<!-- lisa:usage-effectiveness entry_id=(\S+) data=(\S+) -->$/u.exec(
|
|
139
|
+
token[0]
|
|
140
|
+
);
|
|
141
|
+
if (match === null) throw new Error("Invalid effectiveness extension");
|
|
142
|
+
return [
|
|
143
|
+
decodeURIComponent(match[1] ?? ""),
|
|
144
|
+
parseEffectiveness(JSON.parse(decodeURIComponent(match[2] ?? ""))),
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
const entries = new Map(rows);
|
|
149
|
+
if (
|
|
150
|
+
rows.some(
|
|
151
|
+
([id, row]) => JSON.stringify(entries.get(id)) !== JSON.stringify(row)
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
throw new Error("Conflicting effectiveness extensions");
|
|
155
|
+
return entries;
|
|
156
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
import type { LisaUsageSectionIntegrityResult } from "./usage-ledger.mjs";
|
|
5
|
+
export declare function verifyLisaUsageSectionIntegrity(
|
|
6
|
+
storedDocument: string,
|
|
7
|
+
expected?: { entryIds?: readonly string[] }
|
|
8
|
+
): LisaUsageSectionIntegrityResult;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
import { LISA_USAGE_HEADING, parseLisaUsageSection } from "./usage-ledger.mjs";
|
|
5
|
+
/**
|
|
6
|
+
* Sort ids so an integrity report is deterministic regardless of document order.
|
|
7
|
+
*
|
|
8
|
+
* @param ids Entry ids to normalize.
|
|
9
|
+
* @returns The ids in stable sorted order.
|
|
10
|
+
*/
|
|
11
|
+
function sortedIds(ids) {
|
|
12
|
+
return [...ids].sort((left, right) =>
|
|
13
|
+
left > right ? 1 : left < right ? -1 : 0
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Emit an issue only when it actually names something, so callers can compose
|
|
18
|
+
* checks by concatenation instead of conditional accumulation.
|
|
19
|
+
*
|
|
20
|
+
* @param code Stable issue code.
|
|
21
|
+
* @param entryIds Entry ids the issue is about.
|
|
22
|
+
* @param message Operator-readable description.
|
|
23
|
+
* @returns A zero- or one-element issue list.
|
|
24
|
+
*/
|
|
25
|
+
function reportIssue(code, entryIds, message) {
|
|
26
|
+
return entryIds.length === 0 ? [] : [{ code, entryIds, message }];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Report entries the caller just wrote that the stored surface cannot produce.
|
|
30
|
+
*
|
|
31
|
+
* @param expectedEntryIds Entry ids the caller believes it persisted.
|
|
32
|
+
* @param parsedEntryIds Entry ids actually parseable from the stored section.
|
|
33
|
+
* @returns Zero or one `missing-entry-token` issue.
|
|
34
|
+
*/
|
|
35
|
+
function findDroppedExpectedEntries(expectedEntryIds, parsedEntryIds) {
|
|
36
|
+
const missing = sortedIds(
|
|
37
|
+
expectedEntryIds.filter(entryId => !parsedEntryIds.has(entryId))
|
|
38
|
+
);
|
|
39
|
+
return reportIssue(
|
|
40
|
+
"missing-entry-token",
|
|
41
|
+
missing,
|
|
42
|
+
`Stored section does not contain a parseable lisa:usage-entry token for: ${missing.join(", ")}. The write surface dropped them.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Report rollup references that resolve to no direct entry in the same section.
|
|
47
|
+
*
|
|
48
|
+
* @param rollupEntryIds Entry ids named by the stored rollup token.
|
|
49
|
+
* @param parsedEntryIds Entry ids actually parseable from the stored section.
|
|
50
|
+
* @param alreadyReported Ids already reported as dropped caller expectations.
|
|
51
|
+
* @returns Zero or one `missing-entry-token` issue.
|
|
52
|
+
*/
|
|
53
|
+
function findUnresolvableRollupReferences(
|
|
54
|
+
rollupEntryIds,
|
|
55
|
+
parsedEntryIds,
|
|
56
|
+
alreadyReported
|
|
57
|
+
) {
|
|
58
|
+
const unresolvable = sortedIds(
|
|
59
|
+
rollupEntryIds.filter(
|
|
60
|
+
entryId =>
|
|
61
|
+
!parsedEntryIds.has(entryId) && !alreadyReported.includes(entryId)
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
return reportIssue(
|
|
65
|
+
"missing-entry-token",
|
|
66
|
+
unresolvable,
|
|
67
|
+
`Rollup direct_entry_ids names entries that cannot be parsed from the same section: ${unresolvable.join(", ")}.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Report direct entries the stored rollup token fails to account for.
|
|
72
|
+
*
|
|
73
|
+
* @param rollup Stored rollup token, or null when the section carries none.
|
|
74
|
+
* @param parsedEntryIds Entry ids actually parseable from the stored section.
|
|
75
|
+
* @returns Zero or one issue describing the disagreement.
|
|
76
|
+
*/
|
|
77
|
+
function findRollupCoverageGaps(rollup, parsedEntryIds) {
|
|
78
|
+
const entryIds = sortedIds(parsedEntryIds);
|
|
79
|
+
if (rollup === null) {
|
|
80
|
+
// Every section Lisa writes carries a rollup token, even a child-only or
|
|
81
|
+
// fully empty ledger, so a missing token is always corruption -- report
|
|
82
|
+
// it unconditionally rather than routing through reportIssue(), which
|
|
83
|
+
// suppresses issues that name zero entries.
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
code: "missing-rollup-token",
|
|
87
|
+
entryIds,
|
|
88
|
+
message: `Stored section has ${entryIds.length} direct entr${entryIds.length === 1 ? "y" : "ies"} but no lisa:usage-rollup token.`,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
const recorded = new Set(rollup.directEntryIds);
|
|
93
|
+
const unrecorded = entryIds.filter(entryId => !recorded.has(entryId));
|
|
94
|
+
return reportIssue(
|
|
95
|
+
"unrecorded-entry",
|
|
96
|
+
unrecorded,
|
|
97
|
+
`Stored section contains direct entries absent from rollup direct_entry_ids: ${unrecorded.join(", ")}.`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Verify that a STORED managed usage section is still a readable ledger.
|
|
102
|
+
*
|
|
103
|
+
* This exists because a write surface can accept a section, report success, and
|
|
104
|
+
* silently destroy part of it. Callers must run this against the bytes read back
|
|
105
|
+
* from the host, never against the payload they sent and never against the
|
|
106
|
+
* mutation's return value — trusting the mutation result is precisely what let a
|
|
107
|
+
* Linear description strip every entry token while reporting `success: true`.
|
|
108
|
+
*
|
|
109
|
+
* @param storedDocument Artifact body or comment body as read back from the host.
|
|
110
|
+
* @param expected Optional expectations from the write that was just performed.
|
|
111
|
+
* @param expected.entryIds Entry ids the caller believes it just persisted.
|
|
112
|
+
* @returns Whether the stored ledger is enumerable, plus every issue found.
|
|
113
|
+
*/
|
|
114
|
+
export function verifyLisaUsageSectionIntegrity(storedDocument, expected) {
|
|
115
|
+
const parsed = parseLisaUsageSection(storedDocument);
|
|
116
|
+
const expectedEntryIds = expected?.entryIds ?? [];
|
|
117
|
+
if (parsed.range === null) {
|
|
118
|
+
const missing = sortedIds(expectedEntryIds);
|
|
119
|
+
const issues = reportIssue(
|
|
120
|
+
"missing-section",
|
|
121
|
+
missing,
|
|
122
|
+
`Stored artifact has no ${LISA_USAGE_HEADING} section, but ${missing.length} usage entr${missing.length === 1 ? "y was" : "ies were"} written to it.`
|
|
123
|
+
);
|
|
124
|
+
return { ok: issues.length === 0, issues };
|
|
125
|
+
}
|
|
126
|
+
const parsedEntryIds = new Set(parsed.entries.map(entry => entry.entryId));
|
|
127
|
+
const droppedExpected = findDroppedExpectedEntries(
|
|
128
|
+
expectedEntryIds,
|
|
129
|
+
parsedEntryIds
|
|
130
|
+
);
|
|
131
|
+
const issues = [
|
|
132
|
+
...droppedExpected,
|
|
133
|
+
...findUnresolvableRollupReferences(
|
|
134
|
+
parsed.rollup?.directEntryIds ?? [],
|
|
135
|
+
parsedEntryIds,
|
|
136
|
+
droppedExpected.flatMap(issue => issue.entryIds)
|
|
137
|
+
),
|
|
138
|
+
...findRollupCoverageGaps(parsed.rollup, parsedEntryIds),
|
|
139
|
+
];
|
|
140
|
+
return { ok: issues.length === 0, issues };
|
|
141
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
import type { EffectivenessObservation } from "./usage-effectiveness.mjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
export interface LisaUsageEntry {
|
|
10
|
+
artifactRef: string;
|
|
11
|
+
cachedInputTokens: number | null;
|
|
12
|
+
cost: number | null;
|
|
13
|
+
currency: string | null;
|
|
14
|
+
entryId: string;
|
|
15
|
+
effectiveness?: EffectivenessObservation;
|
|
16
|
+
flow: string;
|
|
17
|
+
inputTokens: number | null;
|
|
18
|
+
measuredSubsetTokens?: number | null;
|
|
19
|
+
model: string;
|
|
20
|
+
outputTokens: number | null;
|
|
21
|
+
parentArtifactRef: string | null;
|
|
22
|
+
pricingSource: string | null;
|
|
23
|
+
pricingStatus: string;
|
|
24
|
+
provider: string;
|
|
25
|
+
reasoningTokens: number | null;
|
|
26
|
+
runId: string;
|
|
27
|
+
source: string;
|
|
28
|
+
totalTokens: number | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
*
|
|
33
|
+
*/
|
|
34
|
+
export interface LisaUsageRollup {
|
|
35
|
+
childCost: number | null;
|
|
36
|
+
childCurrency: string | null;
|
|
37
|
+
childEntryIds: readonly string[];
|
|
38
|
+
childRefs: readonly string[];
|
|
39
|
+
childTokens: number | null;
|
|
40
|
+
childTokensIncomplete?: boolean;
|
|
41
|
+
currency: string | null;
|
|
42
|
+
directCost: number | null;
|
|
43
|
+
directEntryIds: readonly string[];
|
|
44
|
+
directTokens: number | null;
|
|
45
|
+
totalCost: number | null;
|
|
46
|
+
totalTokens: number | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
*
|
|
51
|
+
*/
|
|
52
|
+
export interface LisaUsageChildArtifact {
|
|
53
|
+
artifactRef: string;
|
|
54
|
+
entries: readonly LisaUsageEntry[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
*
|
|
59
|
+
*/
|
|
60
|
+
export interface ParsedLisaUsageSection {
|
|
61
|
+
entries: readonly LisaUsageEntry[];
|
|
62
|
+
range: { end: number; start: number } | null;
|
|
63
|
+
rollup: LisaUsageRollup | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Describe one way a stored managed usage section fails to be a readable ledger.
|
|
68
|
+
*/
|
|
69
|
+
export interface LisaUsageSectionIntegrityIssue {
|
|
70
|
+
code:
|
|
71
|
+
| "missing-entry-token"
|
|
72
|
+
| "missing-rollup-token"
|
|
73
|
+
| "missing-section"
|
|
74
|
+
| "unrecorded-entry";
|
|
75
|
+
entryIds: readonly string[];
|
|
76
|
+
message: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
*
|
|
81
|
+
*/
|
|
82
|
+
export interface LisaUsageSectionIntegrityResult {
|
|
83
|
+
issues: readonly LisaUsageSectionIntegrityIssue[];
|
|
84
|
+
ok: boolean;
|
|
85
|
+
}
|
|
86
|
+
export declare const LISA_USAGE_HEADING: "## Lisa Usage";
|
|
87
|
+
export declare function parseLisaUsageSection(
|
|
88
|
+
document: string
|
|
89
|
+
): ParsedLisaUsageSection;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
import { parseEffectivenessTokens } from "./usage-effectiveness.mjs";
|
|
5
|
+
export const LISA_USAGE_HEADING = "## Lisa Usage";
|
|
6
|
+
const ENTRY_TOKEN_PATTERN = /<!-- lisa:usage-entry [^\r\n]*? -->/g;
|
|
7
|
+
const LEGACY_ENTRY_PATTERN =
|
|
8
|
+
/^<!-- lisa:usage-entry entry_id=(\S+) flow=(\S+) run_id=(\S+) provider=(\S+) model=(\S+) source=(\S+) input_tokens=(\S+) cached_input_tokens=(\S+) output_tokens=(\S+) reasoning_tokens=(\S+) total_tokens=(\S+) cost=(\S+) currency=(\S+) pricing_status=(\S+) pricing_source=(\S+) artifact_ref=(\S+) parent_artifact_ref=(\S*) -->$/;
|
|
9
|
+
const RELEASED_MIDDLE_FIELD_ENTRY_PATTERN =
|
|
10
|
+
/^<!-- lisa:usage-entry entry_id=(\S+) flow=(\S+) run_id=(\S+) provider=(\S+) model=(\S+) source=(\S+) input_tokens=(\S+) cached_input_tokens=(\S+) output_tokens=(\S+) reasoning_tokens=(\S+) total_tokens=(\S+) measured_subset_tokens=(\S+) cost=(\S+) currency=(\S+) pricing_status=(\S+) pricing_source=(\S+) artifact_ref=(\S+) parent_artifact_ref=(\S*) -->$/;
|
|
11
|
+
const MEASURED_SUBSET_PATTERN =
|
|
12
|
+
/<!-- lisa:usage-entry-measured-subset entry_id=(\S+) measured_subset_tokens=(\S+) -->/g;
|
|
13
|
+
const ROLLUP_PATTERN =
|
|
14
|
+
/<!-- lisa:usage-rollup direct_entry_ids=(\S*) child_entry_ids=(\S*) child_refs=(\S*) direct_tokens=(\S+) child_tokens=(\S+) total_tokens=(\S+) direct_cost=(\S+) child_cost=(\S+) total_cost=(\S+) currency=(\S+)(?: child_currency=(\S+))? -->/;
|
|
15
|
+
const ROLLUP_TOKEN_STATUS_PATTERN =
|
|
16
|
+
/<!-- lisa:usage-rollup-token-status child_tokens_incomplete=true -->/;
|
|
17
|
+
/**
|
|
18
|
+
* Parse a nullable numeric token field.
|
|
19
|
+
*
|
|
20
|
+
* @param value Serialized numeric token value.
|
|
21
|
+
* @returns The parsed number or null when the token is empty.
|
|
22
|
+
*/
|
|
23
|
+
function parseNullableNumber(value) {
|
|
24
|
+
if (value === "null" || value.length === 0) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const parsed = Number(value);
|
|
28
|
+
if (!Number.isFinite(parsed)) {
|
|
29
|
+
throw new Error(`Invalid Lisa usage numeric token: ${value}`);
|
|
30
|
+
}
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse the additive field from the published 2.222.0 marker. Legacy callers
|
|
35
|
+
* could serialize an omitted property as the literal `undefined`; no other
|
|
36
|
+
* numeric field receives this compatibility exception.
|
|
37
|
+
*
|
|
38
|
+
* @param value Transitional measured-subset token value.
|
|
39
|
+
* @returns The parsed subtotal, or null for the published undefined omission.
|
|
40
|
+
*/
|
|
41
|
+
function parseReleasedMeasuredSubset(value) {
|
|
42
|
+
return value === "undefined" ? null : parseNullableNumber(value);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse a nullable string token field.
|
|
46
|
+
*
|
|
47
|
+
* @param value Serialized string token value.
|
|
48
|
+
* @returns The parsed string or null when the token is empty.
|
|
49
|
+
*/
|
|
50
|
+
function parseNullableString(value) {
|
|
51
|
+
if (value === "null" || value.length === 0) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return decodeTokenValue(value);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Decode a percent-encoded token field.
|
|
58
|
+
*
|
|
59
|
+
* @param value Serialized token field.
|
|
60
|
+
* @returns The decoded token value, or the original value for legacy tokens.
|
|
61
|
+
*/
|
|
62
|
+
function decodeTokenValue(value) {
|
|
63
|
+
try {
|
|
64
|
+
return decodeURIComponent(value);
|
|
65
|
+
} catch {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Parse a comma-separated token field into stable document order.
|
|
71
|
+
*
|
|
72
|
+
* @param value Serialized CSV token field.
|
|
73
|
+
* @returns The parsed item list.
|
|
74
|
+
*/
|
|
75
|
+
function parseCsv(value) {
|
|
76
|
+
if (value.length === 0) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
return value.split(",").map(decodeTokenValue);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Locate the current managed usage-section boundaries inside an artifact body.
|
|
83
|
+
*
|
|
84
|
+
* @param document Artifact markdown to inspect.
|
|
85
|
+
* @returns The start/end character offsets for the section, if present.
|
|
86
|
+
*/
|
|
87
|
+
function findUsageSectionRange(document) {
|
|
88
|
+
const heading = `${LISA_USAGE_HEADING}\n`;
|
|
89
|
+
const start = document.indexOf(heading);
|
|
90
|
+
if (start === -1) {
|
|
91
|
+
if (!document.endsWith(LISA_USAGE_HEADING)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
start: document.length - LISA_USAGE_HEADING.length,
|
|
96
|
+
end: document.length,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const afterHeading = start + heading.length;
|
|
100
|
+
const nextHeadingOffset = document.slice(afterHeading).search(/^##\s+/m);
|
|
101
|
+
const end =
|
|
102
|
+
nextHeadingOffset === -1
|
|
103
|
+
? document.length
|
|
104
|
+
: afterHeading + nextHeadingOffset;
|
|
105
|
+
return { start, end };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Parse the managed rollup and its optional token-completeness extension.
|
|
109
|
+
*
|
|
110
|
+
* @param section Managed usage section text.
|
|
111
|
+
* @returns The parsed rollup, or null when the section has no rollup token.
|
|
112
|
+
*/
|
|
113
|
+
function parseLisaUsageRollup(section) {
|
|
114
|
+
const match = ROLLUP_PATTERN.exec(section);
|
|
115
|
+
if (match === null) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
directEntryIds: parseCsv(match[1] ?? ""),
|
|
120
|
+
childEntryIds: parseCsv(match[2] ?? ""),
|
|
121
|
+
childRefs: parseCsv(match[3] ?? ""),
|
|
122
|
+
directTokens: parseNullableNumber(match[4] ?? ""),
|
|
123
|
+
childTokens: parseNullableNumber(match[5] ?? ""),
|
|
124
|
+
...(ROLLUP_TOKEN_STATUS_PATTERN.test(section)
|
|
125
|
+
? { childTokensIncomplete: true }
|
|
126
|
+
: {}),
|
|
127
|
+
totalTokens: parseNullableNumber(match[6] ?? ""),
|
|
128
|
+
directCost: parseNullableNumber(match[7] ?? ""),
|
|
129
|
+
childCost: parseNullableNumber(match[8] ?? ""),
|
|
130
|
+
totalCost: parseNullableNumber(match[9] ?? ""),
|
|
131
|
+
currency: parseNullableString(match[10] ?? ""),
|
|
132
|
+
childCurrency: parseNullableString(match[11] ?? ""),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Parse the managed usage section out of an artifact body or comment.
|
|
137
|
+
*
|
|
138
|
+
* @param document Artifact markdown to inspect.
|
|
139
|
+
* @returns Parsed direct entries, rollup token, and the located section range.
|
|
140
|
+
*/
|
|
141
|
+
export function parseLisaUsageSection(document) {
|
|
142
|
+
const range = findUsageSectionRange(document);
|
|
143
|
+
const section = range ? document.slice(range.start, range.end) : "";
|
|
144
|
+
const effectiveness = parseEffectivenessTokens(section);
|
|
145
|
+
const measuredSubsets = new Map(
|
|
146
|
+
Array.from(section.matchAll(MEASURED_SUBSET_PATTERN), match => [
|
|
147
|
+
decodeTokenValue(match[1] ?? ""),
|
|
148
|
+
parseNullableNumber(match[2] ?? ""),
|
|
149
|
+
])
|
|
150
|
+
);
|
|
151
|
+
const entries = Array.from(
|
|
152
|
+
section.matchAll(ENTRY_TOKEN_PATTERN),
|
|
153
|
+
tokenMatch => {
|
|
154
|
+
const token = tokenMatch[0];
|
|
155
|
+
const releasedMatch = RELEASED_MIDDLE_FIELD_ENTRY_PATTERN.exec(token);
|
|
156
|
+
const match = releasedMatch ?? LEGACY_ENTRY_PATTERN.exec(token);
|
|
157
|
+
if (match === null) {
|
|
158
|
+
throw new Error(`Invalid Lisa usage entry token: ${token}`);
|
|
159
|
+
}
|
|
160
|
+
const releasedFieldOffset = releasedMatch === null ? 0 : 1;
|
|
161
|
+
const entryId = decodeTokenValue(match[1] ?? "");
|
|
162
|
+
const embeddedMeasuredSubset =
|
|
163
|
+
releasedMatch === null
|
|
164
|
+
? null
|
|
165
|
+
: parseReleasedMeasuredSubset(match[12] ?? "");
|
|
166
|
+
return {
|
|
167
|
+
entryId,
|
|
168
|
+
...(effectiveness.has(entryId)
|
|
169
|
+
? { effectiveness: effectiveness.get(entryId) }
|
|
170
|
+
: {}),
|
|
171
|
+
flow: decodeTokenValue(match[2] ?? ""),
|
|
172
|
+
runId: decodeTokenValue(match[3] ?? ""),
|
|
173
|
+
provider: decodeTokenValue(match[4] ?? ""),
|
|
174
|
+
model: decodeTokenValue(match[5] ?? ""),
|
|
175
|
+
source: decodeTokenValue(match[6] ?? ""),
|
|
176
|
+
inputTokens: parseNullableNumber(match[7] ?? ""),
|
|
177
|
+
cachedInputTokens: parseNullableNumber(match[8] ?? ""),
|
|
178
|
+
outputTokens: parseNullableNumber(match[9] ?? ""),
|
|
179
|
+
reasoningTokens: parseNullableNumber(match[10] ?? ""),
|
|
180
|
+
totalTokens: parseNullableNumber(match[11] ?? ""),
|
|
181
|
+
measuredSubsetTokens: measuredSubsets.has(entryId)
|
|
182
|
+
? (measuredSubsets.get(entryId) ?? null)
|
|
183
|
+
: embeddedMeasuredSubset,
|
|
184
|
+
cost: parseNullableNumber(match[12 + releasedFieldOffset] ?? ""),
|
|
185
|
+
currency: parseNullableString(match[13 + releasedFieldOffset] ?? ""),
|
|
186
|
+
pricingStatus: decodeTokenValue(match[14 + releasedFieldOffset] ?? ""),
|
|
187
|
+
pricingSource: parseNullableString(
|
|
188
|
+
match[15 + releasedFieldOffset] ?? ""
|
|
189
|
+
),
|
|
190
|
+
artifactRef: decodeTokenValue(match[16 + releasedFieldOffset] ?? ""),
|
|
191
|
+
parentArtifactRef: parseNullableString(
|
|
192
|
+
match[17 + releasedFieldOffset] ?? ""
|
|
193
|
+
),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
const entryIds = new Set(entries.map(entry => entry.entryId));
|
|
198
|
+
for (const entryId of effectiveness.keys()) {
|
|
199
|
+
if (!entryIds.has(entryId)) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Effectiveness observation has no usage entry: ${entryId}`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return { entries, rollup: parseLisaUsageRollup(section), range };
|
|
206
|
+
}
|
|
@@ -3475,8 +3475,10 @@ if [ -f "$rules_file" ]; then
|
|
|
3475
3475
|
# here — before the remaining rules ran, and with an exit status that is
|
|
3476
3476
|
# not a refusal. The `||` puts the pipeline in a condition context, where
|
|
3477
3477
|
# a non-zero status is expected rather than fatal.
|
|
3478
|
+
# Consume the whole input: grep -q can close an early match while printf
|
|
3479
|
+
# is still writing, making pipefail report SIGPIPE instead of the match.
|
|
3478
3480
|
rule_status=0
|
|
3479
|
-
printf '%s' "$custom_command_str" | grep -
|
|
3481
|
+
printf '%s' "$custom_command_str" | grep -Ei -- "$rule" >/dev/null || rule_status=$?
|
|
3480
3482
|
case "$rule_status" in
|
|
3481
3483
|
0) block "matched a project custom safety rule (${rules_file##*/}): $rule" ;;
|
|
3482
3484
|
1) ;; # no match
|