@codyswann/lisa 3.14.1 → 3.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all/copy-overwrite/scripts/lisa-destructive-guard.mjs +3 -0
- package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +3 -0
- package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +3 -0
- package/all/copy-overwrite/scripts/lisa-hooks/block-instruction-file-edits.sh +3 -0
- package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +3 -0
- package/all/copy-overwrite/scripts/lisa-hooks/block-shell-json-parsing.sh +3 -0
- package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +3 -0
- package/all/copy-overwrite/scripts/lisa-hooks/sonar-secrets.sh +3 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +11 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +34 -29
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/copy-overwrite/.easignore.extra +3 -0
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- 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-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- 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-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-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-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-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-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-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-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/rails/copy-overwrite/.rubocop.yml +3 -0
- package/rails/copy-overwrite/ast-grep/rules/ruby/no-find-by-sql-without-params.yml +3 -0
- package/rails/copy-overwrite/ast-grep/rules/ruby/no-params-without-permit.yml +3 -0
- package/rails/copy-overwrite/ast-grep/rules/ruby/no-raw-sql-in-where.yml +3 -0
- package/rails/copy-overwrite/ast-grep/rules/ruby/no-skip-before-action-without-scope.yml +3 -0
- package/rails/copy-overwrite/ast-grep/rules/ruby/no-unsafe-send.yml +3 -0
- package/rails/copy-overwrite/ast-grep/rules/ruby/no-update-columns.yml +3 -0
- package/rails/copy-overwrite/config/initializers/version.rb +2 -0
- package/rails/copy-overwrite/lefthook.yml +3 -0
- package/rails/copy-overwrite/scripts/check-threshold-ratchet.mjs +3 -0
- package/rails/copy-overwrite/scripts/threshold-ratchet-compare.mjs +3 -0
- package/rails/copy-overwrite/scripts/threshold-ratchet-families.mjs +3 -0
- package/scripts/build-plugins.sh +23 -7
- package/scripts/check-derived-artifacts.mjs +237 -0
- package/scripts/materialize-copy-overwrite.mjs +163 -0
- package/typescript/copy-contents/.husky/pre-commit +16 -0
- package/typescript/copy-overwrite/.github/GITHUB_ACTIONS.md +3 -0
- package/typescript/copy-overwrite/ast-grep/rules/no-inline-component-in-container.yml +3 -0
- package/typescript/copy-overwrite/ast-grep/rules/no-inline-component-in-view.yml +3 -0
- package/typescript/copy-overwrite/scripts/check-threshold-ratchet.mjs +3 -0
- package/typescript/copy-overwrite/scripts/threshold-ratchet-compare.mjs +3 -0
- package/typescript/copy-overwrite/scripts/threshold-ratchet-families.mjs +3 -0
package/scripts/build-plugins.sh
CHANGED
|
@@ -12,6 +12,20 @@ SRC_DIR="$PLUGINS_DIR/src"
|
|
|
12
12
|
# Read version from package.json so plugins stay in sync with Lisa releases
|
|
13
13
|
VERSION=$(node -e "console.log(require('$ROOT_DIR/package.json').version)")
|
|
14
14
|
|
|
15
|
+
# Materialize a Lisa-owned source file into a copy-overwrite/ template tree.
|
|
16
|
+
#
|
|
17
|
+
# Never a plain `cp`. These destinations are `copy-overwrite` assets, and a
|
|
18
|
+
# `copy-overwrite` asset that reads as editable silently loses downstream
|
|
19
|
+
# hardening on the next sync — already observed on block-no-verify.sh. They are
|
|
20
|
+
# also the only assets that cannot carry a hand-typed ownership header, because
|
|
21
|
+
# this script would erase it on the next build (#2547). So the header is stamped
|
|
22
|
+
# here, as the file is generated: the authored source stays honest about being
|
|
23
|
+
# editable, the shipped copy states that it is replaced, and the two cannot
|
|
24
|
+
# disagree because one produces the other.
|
|
25
|
+
materialize() {
|
|
26
|
+
node "$ROOT_DIR/scripts/materialize-copy-overwrite.mjs" "$1" "$2"
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
inject_version() {
|
|
16
30
|
local manifest="$1"
|
|
17
31
|
if [ -f "$manifest" ]; then
|
|
@@ -82,11 +96,12 @@ if [ -f "$SRC_DIR/base/hooks/threshold-ratchet.mjs" ]; then
|
|
|
82
96
|
mkdir -p "$ratchet_scripts_dir"
|
|
83
97
|
# The entry point takes the template check-* naming; its relative imports
|
|
84
98
|
# (threshold-ratchet-*.mjs) keep their canonical names in both trees.
|
|
85
|
-
|
|
99
|
+
materialize "$SRC_DIR/base/hooks/threshold-ratchet.mjs" \
|
|
86
100
|
"$ratchet_scripts_dir/check-threshold-ratchet.mjs"
|
|
87
|
-
|
|
88
|
-
"$SRC_DIR/base/hooks
|
|
89
|
-
|
|
101
|
+
for ratchet_module in threshold-ratchet-families threshold-ratchet-compare; do
|
|
102
|
+
materialize "$SRC_DIR/base/hooks/$ratchet_module.mjs" \
|
|
103
|
+
"$ratchet_scripts_dir/$ratchet_module.mjs"
|
|
104
|
+
done
|
|
90
105
|
done
|
|
91
106
|
fi
|
|
92
107
|
|
|
@@ -108,7 +123,7 @@ fi
|
|
|
108
123
|
for guard in block-no-verify parity-safety-net block-shell-json-parsing \
|
|
109
124
|
block-instruction-file-edits block-direct-issue-create; do
|
|
110
125
|
if [ -f "$SRC_DIR/base/hooks/$guard.sh" ]; then
|
|
111
|
-
|
|
126
|
+
materialize "$SRC_DIR/base/hooks/$guard.sh" "$HOST_GUARD_DIR/$guard.sh"
|
|
112
127
|
chmod +x "$HOST_GUARD_DIR/$guard.sh"
|
|
113
128
|
fi
|
|
114
129
|
done
|
|
@@ -122,7 +137,8 @@ done
|
|
|
122
137
|
# guards are, not because it is dispatched the same way.
|
|
123
138
|
if [ -f "$SRC_DIR/base/hooks/sonar-secrets.sh" ]; then
|
|
124
139
|
mkdir -p "$HOST_GUARD_DIR"
|
|
125
|
-
|
|
140
|
+
materialize "$SRC_DIR/base/hooks/sonar-secrets.sh" \
|
|
141
|
+
"$HOST_GUARD_DIR/sonar-secrets.sh"
|
|
126
142
|
chmod +x "$HOST_GUARD_DIR/sonar-secrets.sh"
|
|
127
143
|
fi
|
|
128
144
|
|
|
@@ -132,7 +148,7 @@ fi
|
|
|
132
148
|
# and an unconditional copy fails the whole build there.
|
|
133
149
|
if [ -f "$ROOT_DIR/scripts/lisa-enforcement-fallback.sh" ]; then
|
|
134
150
|
mkdir -p "$ROOT_DIR/all/copy-overwrite/scripts"
|
|
135
|
-
|
|
151
|
+
materialize "$ROOT_DIR/scripts/lisa-enforcement-fallback.sh" \
|
|
136
152
|
"$ROOT_DIR/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh"
|
|
137
153
|
chmod +x "$ROOT_DIR/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh"
|
|
138
154
|
fi
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Fail a commit whose generated artifacts no longer vouch for the bytes beside
|
|
4
|
+
* them.
|
|
5
|
+
*
|
|
6
|
+
* Lisa has two artifacts derived from tracked sources:
|
|
7
|
+
*
|
|
8
|
+
* - `src/core/upstream-evidence-manifest.ts`, regenerated by
|
|
9
|
+
* `bun run build:upstream-evidence-manifest`
|
|
10
|
+
* - `src/core/lisa-owned-hash-ledger.ts`, regenerated by
|
|
11
|
+
* `bun run build:lisa-owned-hash-ledger`
|
|
12
|
+
*
|
|
13
|
+
* Each already has a suite-level guard, but the suite is the slow and late place
|
|
14
|
+
* to learn it. CodySwannGT/lisa#2557 is what that costs: three branches, each
|
|
15
|
+
* green on its own, combined into a red `main` that only failed in a post-merge
|
|
16
|
+
* Release run. The second obligation was days old and in nobody's checklist, and
|
|
17
|
+
* the measured pattern in this repository is that executable controls hold where
|
|
18
|
+
* prose does not — so this is the control, at the earliest point that can name
|
|
19
|
+
* the command to run.
|
|
20
|
+
*
|
|
21
|
+
* **This script verifies nothing itself.** It delegates to each generator's own
|
|
22
|
+
* `--check`, which is the point: a second implementation of "is the artifact
|
|
23
|
+
* current" would be a second thing to keep in step. The two checks deliberately
|
|
24
|
+
* assert different properties, and both are correct for their artifact:
|
|
25
|
+
*
|
|
26
|
+
* - The manifest hashes tracked files with no history walk, so a fresh
|
|
27
|
+
* regeneration is byte-reproducible anywhere and byte-identity is a fair test.
|
|
28
|
+
* - The ledger walks history, whose result depends on clone depth and merge
|
|
29
|
+
* topology. It therefore asserts only that the bytes shipped *right now* are
|
|
30
|
+
* recorded — the property that actually protects refresh. Asserting
|
|
31
|
+
* byte-identity there made a correct ledger fail once `autoupdate` merged
|
|
32
|
+
* `main` in (CodySwannGT/lisa#2556), and at this repository's concurrency that
|
|
33
|
+
* would mean every merge reddens every open PR. Do not tighten it back.
|
|
34
|
+
*
|
|
35
|
+
* Both triggers below are deliberate **supersets** of what each artifact covers.
|
|
36
|
+
* Re-deriving exact membership here would create a second copy of the rule that
|
|
37
|
+
* could drift and silently stop firing; over-triggering only costs a few seconds
|
|
38
|
+
* on a commit that owed nothing, which is the safe direction to be wrong in.
|
|
39
|
+
* @module scripts/check-derived-artifacts
|
|
40
|
+
*/
|
|
41
|
+
import { execFileSync } from "node:child_process";
|
|
42
|
+
import path from "node:path";
|
|
43
|
+
import process from "node:process";
|
|
44
|
+
import { fileURLToPath } from "node:url";
|
|
45
|
+
|
|
46
|
+
const repoRoot = path.resolve(import.meta.dirname, "..");
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Path prefixes whose contents the upstream evidence manifest hash-pins.
|
|
50
|
+
*
|
|
51
|
+
* Mirrors `packagedEvidencePrefixes` in
|
|
52
|
+
* `scripts/generate-upstream-evidence-manifest.mjs`. The duplication is
|
|
53
|
+
* deliberate: that script runs top-level code which refuses to load outside the
|
|
54
|
+
* canonical repository, so it cannot be imported for its constant. A test
|
|
55
|
+
* asserts the two lists agree, the same arrangement `isLisaOwned` already uses
|
|
56
|
+
* in the ledger generator.
|
|
57
|
+
* @type {readonly string[]}
|
|
58
|
+
*/
|
|
59
|
+
export const PACKAGED_EVIDENCE_PREFIXES = Object.freeze([
|
|
60
|
+
"plugins/src/",
|
|
61
|
+
"all/",
|
|
62
|
+
"cdk/",
|
|
63
|
+
"expo/",
|
|
64
|
+
"eslint-plugin-code-organization/",
|
|
65
|
+
"eslint-plugin-component-structure/",
|
|
66
|
+
"eslint-plugin-phaser/",
|
|
67
|
+
"eslint-plugin-ui-standards/",
|
|
68
|
+
"harper-fabric/",
|
|
69
|
+
"nestjs/",
|
|
70
|
+
"npm-package/",
|
|
71
|
+
"oxlint/",
|
|
72
|
+
"phaser/",
|
|
73
|
+
"rails/",
|
|
74
|
+
"scripts/",
|
|
75
|
+
"tsconfig/",
|
|
76
|
+
"typescript/",
|
|
77
|
+
"ui/",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
/** Marker separating a template's source location from its installed path. */
|
|
81
|
+
const COPY_OVERWRITE = "/copy-overwrite/";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether this change can move the bytes of a template the ledger vouches for.
|
|
85
|
+
*
|
|
86
|
+
* Every ledger source lives under a `copy-overwrite` directory, so the marker is
|
|
87
|
+
* a superset of the `lisa-` namespace the ledger actually records. Matching the
|
|
88
|
+
* broader set means a template that is renamed *into* the namespace, or added to
|
|
89
|
+
* it later, still trips the gate.
|
|
90
|
+
* @param {readonly string[]} stagedPaths - Repo-relative staged paths
|
|
91
|
+
* @returns {boolean} True when the ledger check is owed
|
|
92
|
+
*/
|
|
93
|
+
export function requiresLedgerCheck(stagedPaths) {
|
|
94
|
+
return stagedPaths.some(staged => staged.includes(COPY_OVERWRITE));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether this change touches a path the evidence manifest hash-pins.
|
|
99
|
+
* @param {readonly string[]} stagedPaths - Repo-relative staged paths
|
|
100
|
+
* @returns {boolean} True when the manifest check is owed
|
|
101
|
+
*/
|
|
102
|
+
export function requiresManifestCheck(stagedPaths) {
|
|
103
|
+
return stagedPaths.some(staged =>
|
|
104
|
+
PACKAGED_EVIDENCE_PREFIXES.some(prefix => staged.startsWith(prefix))
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Repo-relative paths staged for the pending commit.
|
|
110
|
+
*
|
|
111
|
+
* Renames report their destination (`-M` plus `--diff-filter=R`), and deletions
|
|
112
|
+
* are included because removing a template is also a way to move what the
|
|
113
|
+
* artifacts describe.
|
|
114
|
+
* @returns {string[]} Staged paths, empty when git cannot be consulted
|
|
115
|
+
*/
|
|
116
|
+
export function stagedPaths() {
|
|
117
|
+
try {
|
|
118
|
+
return execFileSync(
|
|
119
|
+
"git",
|
|
120
|
+
["diff", "--cached", "--name-only", "--diff-filter=ACMRD"],
|
|
121
|
+
{ cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
122
|
+
)
|
|
123
|
+
.split("\n")
|
|
124
|
+
.filter(Boolean);
|
|
125
|
+
} catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The human-readable half of a generator's failure output.
|
|
132
|
+
*
|
|
133
|
+
* A generator that reports by throwing prints a Node stack trace around its
|
|
134
|
+
* message. The frames are noise to the person being blocked — and this gate is
|
|
135
|
+
* read by operators who did not write either generator — so keep the lines that
|
|
136
|
+
* say what is wrong and drop the ones that say where the throw was.
|
|
137
|
+
* @param {string} raw - Combined stdout and stderr from the generator
|
|
138
|
+
* @returns {string} Message lines, stack frames removed
|
|
139
|
+
*/
|
|
140
|
+
export function readableFailure(raw) {
|
|
141
|
+
const lines = raw.split("\n");
|
|
142
|
+
// Node prints `<file>:<line>`, the offending source line, then a caret, before
|
|
143
|
+
// the message itself. Everything through the caret is the code frame.
|
|
144
|
+
const caret = lines.findIndex(line => /^\s*\^+\s*$/u.test(line));
|
|
145
|
+
return lines
|
|
146
|
+
.slice(caret + 1)
|
|
147
|
+
.filter(line => !/^\s+at\s/u.test(line))
|
|
148
|
+
.filter(line => !line.startsWith("Node.js v"))
|
|
149
|
+
.map(line => line.replace(/^Error:\s*/u, "").trimEnd())
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
.join("\n")
|
|
152
|
+
.trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Run one generator's `--check` and report whether it passed.
|
|
157
|
+
* @param {string} script - Repo-relative generator path
|
|
158
|
+
* @returns {{ok: boolean, output: string}} Result and any diagnostic output
|
|
159
|
+
*/
|
|
160
|
+
function runCheck(script) {
|
|
161
|
+
try {
|
|
162
|
+
execFileSync(process.execPath, [script, "--check"], {
|
|
163
|
+
cwd: repoRoot,
|
|
164
|
+
encoding: "utf8",
|
|
165
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
166
|
+
});
|
|
167
|
+
return { ok: true, output: "" };
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const stderr = error?.stderr ?? "";
|
|
170
|
+
const stdout = error?.stdout ?? "";
|
|
171
|
+
return { ok: false, output: readableFailure(`${stdout}${stderr}`) };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The artifacts this gate knows how to check, in the order they are reported.
|
|
177
|
+
* @type {readonly {name: string, script: string, command: string, trigger: (paths: readonly string[]) => boolean}[]}
|
|
178
|
+
*/
|
|
179
|
+
const ARTIFACTS = Object.freeze([
|
|
180
|
+
Object.freeze({
|
|
181
|
+
name: "upstream evidence manifest",
|
|
182
|
+
script: "scripts/generate-upstream-evidence-manifest.mjs",
|
|
183
|
+
command: "bun run build:upstream-evidence-manifest",
|
|
184
|
+
trigger: requiresManifestCheck,
|
|
185
|
+
}),
|
|
186
|
+
Object.freeze({
|
|
187
|
+
name: "Lisa-owned hash ledger",
|
|
188
|
+
script: "scripts/generate-lisa-owned-hash-ledger.mjs",
|
|
189
|
+
command: "bun run build:lisa-owned-hash-ledger",
|
|
190
|
+
trigger: requiresLedgerCheck,
|
|
191
|
+
}),
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Check every artifact this commit owes and describe any that are stale.
|
|
196
|
+
* @param {readonly string[]} staged - Repo-relative staged paths
|
|
197
|
+
* @returns {string[]} One remediation block per stale artifact
|
|
198
|
+
*/
|
|
199
|
+
export function staleArtifacts(staged) {
|
|
200
|
+
const failures = [];
|
|
201
|
+
for (const artifact of ARTIFACTS) {
|
|
202
|
+
if (!artifact.trigger(staged)) continue;
|
|
203
|
+
const result = runCheck(artifact.script);
|
|
204
|
+
if (result.ok) continue;
|
|
205
|
+
failures.push(
|
|
206
|
+
`${artifact.name} is stale.\n` +
|
|
207
|
+
`${result.output ? `${result.output}\n` : ""}` +
|
|
208
|
+
` Fix: ${artifact.command}\n` +
|
|
209
|
+
` Then: git add the regenerated file and commit again.`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return failures;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Run the gate over the staged change and exit non-zero when anything is stale. */
|
|
216
|
+
function main() {
|
|
217
|
+
const staged = stagedPaths();
|
|
218
|
+
const failures = staleArtifacts(staged);
|
|
219
|
+
if (failures.length === 0) {
|
|
220
|
+
process.stdout.write("Generated artifacts are current.\n");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
process.stderr.write(
|
|
224
|
+
`\nA generated artifact no longer matches the sources in this commit:\n\n${failures.join(
|
|
225
|
+
"\n\n"
|
|
226
|
+
)}\n\n` +
|
|
227
|
+
`Both artifacts are regenerated from the working tree, so run the command\n` +
|
|
228
|
+
`above AFTER staging, and again after any reformat, then amend.\n` +
|
|
229
|
+
`If the stale file is one you did not touch, an unstaged local edit to a\n` +
|
|
230
|
+
`tracked file can also trip this — commit or revert it first.\n`
|
|
231
|
+
);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
236
|
+
main();
|
|
237
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copy a Lisa-owned file into a `copy-overwrite/` template tree, stamping the
|
|
4
|
+
* ownership header on the way in.
|
|
5
|
+
*
|
|
6
|
+
* Thirteen `copy-overwrite` assets are not authored where they ship. The build
|
|
7
|
+
* materializes them from `plugins/src/base/hooks/` (the five PreToolUse guards,
|
|
8
|
+
* the Sonar wrapper, the three threshold-ratchet modules into two stack lanes)
|
|
9
|
+
* and from `scripts/lisa-enforcement-fallback.sh` (the dispatcher). A header
|
|
10
|
+
* typed into the shipped copy is erased by the next `bun run build`, which is
|
|
11
|
+
* why those thirteen were the only files #2545 could not correct.
|
|
12
|
+
*
|
|
13
|
+
* The header cannot move upstream to the source either. `plugins/src/base/hooks/
|
|
14
|
+
* block-no-verify.sh` is precisely the file maintainers edit, so telling it that
|
|
15
|
+
* it will be overwritten would be a fresh instance of the false statement #2538
|
|
16
|
+
* exists to remove. So the header becomes a property of the *generation step*:
|
|
17
|
+
* the source stays honest, the shipped copy states its real contract, and the
|
|
18
|
+
* two can never disagree because one produces the other.
|
|
19
|
+
*
|
|
20
|
+
* This matters most on exactly these files. A `copy-overwrite` asset that reads
|
|
21
|
+
* as editable silently loses downstream hardening — a consumer repo hardened its
|
|
22
|
+
* `block-no-verify.sh` and the next sync reverted it, noticed only because two
|
|
23
|
+
* of its own tests started failing.
|
|
24
|
+
* @module scripts/materialize-copy-overwrite
|
|
25
|
+
*/
|
|
26
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The `copy-overwrite` ownership contract, verbatim from the header #2545
|
|
32
|
+
* established. Duplicated nowhere: `tests/unit/templates/
|
|
33
|
+
* template-ownership-header.test.ts` asserts the same two sentences over every
|
|
34
|
+
* template in the lane, and this generator writes them into the thirteen that
|
|
35
|
+
* cannot hold a hand-typed copy.
|
|
36
|
+
*/
|
|
37
|
+
export const OWNERSHIP_HEADER = [
|
|
38
|
+
"This file is managed by Lisa and IS replaced on each `lisa` run.",
|
|
39
|
+
"Do not edit directly — durable changes belong upstream in Lisa.",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Line comment syntax per extension, for the formats this generator emits.
|
|
44
|
+
*
|
|
45
|
+
* Deliberately a closed map rather than a default. A file type that is not
|
|
46
|
+
* listed makes `materialize` throw, so wiring a new generated asset through
|
|
47
|
+
* here is a decision someone has to make — the alternative is a silent
|
|
48
|
+
* pass-through that reintroduces the headerless copy this module exists to end.
|
|
49
|
+
*/
|
|
50
|
+
const COMMENT_PREFIX = new Map([
|
|
51
|
+
[".sh", "#"],
|
|
52
|
+
[".bash", "#"],
|
|
53
|
+
[".mjs", "//"],
|
|
54
|
+
[".cjs", "//"],
|
|
55
|
+
[".js", "//"],
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The comment prefix a generated file's format uses.
|
|
60
|
+
* @param {string} filePath - Path whose extension selects the syntax.
|
|
61
|
+
* @returns {string | undefined} Prefix, or undefined when unsupported.
|
|
62
|
+
*/
|
|
63
|
+
export const commentPrefixFor = filePath =>
|
|
64
|
+
COMMENT_PREFIX.get(path.extname(filePath));
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The header block as it appears in a file of the given comment syntax.
|
|
68
|
+
* @param {string} prefix - Line comment prefix, e.g. `#` or `//`.
|
|
69
|
+
* @returns {string[]} The header lines, ready to splice in.
|
|
70
|
+
*/
|
|
71
|
+
const headerLines = prefix => OWNERSHIP_HEADER.map(line => `${prefix} ${line}`);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether contents already state the ownership contract.
|
|
75
|
+
* @param {string} contents - File contents.
|
|
76
|
+
* @returns {boolean} True when every header sentence is present.
|
|
77
|
+
*/
|
|
78
|
+
export const hasOwnershipHeader = contents =>
|
|
79
|
+
OWNERSHIP_HEADER.every(line => contents.includes(line));
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Contents with the ownership header stamped in.
|
|
83
|
+
*
|
|
84
|
+
* Placement is not cosmetic: a `#!` line that stops being line 1 stops being a
|
|
85
|
+
* shebang, so the header lands *below* it when one is present. Idempotent, so
|
|
86
|
+
* re-running the build over an already-stamped tree is a no-op rather than a
|
|
87
|
+
* pile of headers.
|
|
88
|
+
* @param {string} contents - Source file contents.
|
|
89
|
+
* @param {string} filePath - Destination path, for its extension.
|
|
90
|
+
* @returns {string} Contents carrying the header.
|
|
91
|
+
* @throws {Error} When the destination's format has no known comment syntax.
|
|
92
|
+
*/
|
|
93
|
+
export const withOwnershipHeader = (contents, filePath) => {
|
|
94
|
+
const prefix = commentPrefixFor(filePath);
|
|
95
|
+
if (prefix === undefined) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`No comment syntax known for ${path.basename(filePath)} — a generated ` +
|
|
98
|
+
`copy-overwrite asset must be able to state its ownership contract.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (hasOwnershipHeader(contents)) return contents;
|
|
102
|
+
|
|
103
|
+
const lines = contents.split("\n");
|
|
104
|
+
const insertAt = lines[0]?.startsWith("#!") ? 1 : 0;
|
|
105
|
+
return [
|
|
106
|
+
...lines.slice(0, insertAt),
|
|
107
|
+
...headerLines(prefix),
|
|
108
|
+
"",
|
|
109
|
+
...lines.slice(insertAt),
|
|
110
|
+
].join("\n");
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Contents with the ownership header removed, leaving the authored source.
|
|
115
|
+
*
|
|
116
|
+
* The byte-equality tests that pin a shipped copy to its reviewed original run
|
|
117
|
+
* through here, so "generated file minus its stamp" has exactly one definition
|
|
118
|
+
* and cannot drift from what the stamp step actually writes.
|
|
119
|
+
* @param {string} contents - Generated file contents.
|
|
120
|
+
* @param {string} filePath - Path, for its comment syntax.
|
|
121
|
+
* @returns {string} Contents without the header block.
|
|
122
|
+
*/
|
|
123
|
+
export const withoutOwnershipHeader = (contents, filePath) => {
|
|
124
|
+
const prefix = commentPrefixFor(filePath);
|
|
125
|
+
if (prefix === undefined) return contents;
|
|
126
|
+
|
|
127
|
+
const block = headerLines(prefix);
|
|
128
|
+
const lines = contents.split("\n");
|
|
129
|
+
const at = lines.findIndex(
|
|
130
|
+
(line, index) => line === block[0] && lines[index + 1] === block[1]
|
|
131
|
+
);
|
|
132
|
+
if (at === -1) return contents;
|
|
133
|
+
|
|
134
|
+
// The blank separator belongs to the stamp, not to the source, so it comes
|
|
135
|
+
// out with it — otherwise round-tripping would grow a line each build.
|
|
136
|
+
const end =
|
|
137
|
+
lines[at + block.length] === "" ? at + block.length + 1 : at + block.length;
|
|
138
|
+
return [...lines.slice(0, at), ...lines.slice(end)].join("\n");
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Read a source file and write it to a destination carrying the header.
|
|
143
|
+
* @param {string} source - Authored file to materialize.
|
|
144
|
+
* @param {string} destination - Path inside a `copy-overwrite/` tree.
|
|
145
|
+
* @returns {void}
|
|
146
|
+
*/
|
|
147
|
+
export const materialize = (source, destination) => {
|
|
148
|
+
writeFileSync(
|
|
149
|
+
destination,
|
|
150
|
+
withOwnershipHeader(readFileSync(source, "utf8"), destination)
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
155
|
+
const [source, destination] = process.argv.slice(2);
|
|
156
|
+
if (!source || !destination) {
|
|
157
|
+
console.error(
|
|
158
|
+
"usage: materialize-copy-overwrite.mjs <source> <destination>"
|
|
159
|
+
);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
materialize(source, destination);
|
|
163
|
+
}
|
|
@@ -129,4 +129,20 @@ fi
|
|
|
129
129
|
# Run lint-staged for incremental lint and format checks
|
|
130
130
|
echo "🚀 Running lint-staged..."
|
|
131
131
|
$EXECUTOR lint-staged --config .lintstagedrc.json
|
|
132
|
+
LINT_STAGED_STATUS=$?
|
|
133
|
+
if [ $LINT_STAGED_STATUS -ne 0 ]; then
|
|
134
|
+
exit $LINT_STAGED_STATUS
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# Generated artifacts must vouch for the bytes committed beside them. Runs AFTER
|
|
138
|
+
# lint-staged on purpose: reformatting a staged template moves its bytes, so a
|
|
139
|
+
# check placed above would sign off on bytes that no longer exist. Absent in host
|
|
140
|
+
# projects, which have the hook but none of Lisa's generators.
|
|
141
|
+
if [ -f "scripts/check-derived-artifacts.mjs" ] && command -v node >/dev/null 2>&1; then
|
|
142
|
+
echo "🧾 Checking generated artifacts..."
|
|
143
|
+
node scripts/check-derived-artifacts.mjs --staged
|
|
144
|
+
if [ $? -ne 0 ]; then
|
|
145
|
+
exit 1
|
|
146
|
+
fi
|
|
147
|
+
fi
|
|
132
148
|
# END: AI GUARDRAILS
|
|
@@ -1,3 +1,6 @@
|
|
|
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
|
+
|
|
1
4
|
# GitHub Actions Configuration
|
|
2
5
|
|
|
3
6
|
This directory contains the CI/CD workflows and automation for the project. This document explains how to configure and use the GitHub Actions workflows.
|