@codyswann/lisa 3.58.0 → 3.59.1
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/{scripts → all/copy-overwrite/scripts}/check-conflict-markers.mjs +15 -8
- package/all/copy-overwrite/scripts/lisa-gates.mjs +1006 -14
- package/dist/cli/gate-report-upstream.d.ts +33 -4
- package/dist/cli/gate-report-upstream.d.ts.map +1 -1
- package/dist/cli/gate-report-upstream.js +69 -11
- package/dist/cli/gate-report-upstream.js.map +1 -1
- package/dist/cli/gate-report.d.ts.map +1 -1
- package/dist/cli/gate-report.js +2 -10
- package/dist/cli/gate-report.js.map +1 -1
- package/dist/configs/vitest/base.d.ts +16 -12
- package/dist/configs/vitest/base.d.ts.map +1 -1
- package/dist/configs/vitest/base.js +51 -0
- package/dist/configs/vitest/base.js.map +1 -1
- package/dist/configs/vitest/cdk.d.ts +2 -2
- package/dist/configs/vitest/cdk.d.ts.map +1 -1
- package/dist/configs/vitest/cdk.js +4 -2
- package/dist/configs/vitest/cdk.js.map +1 -1
- package/dist/configs/vitest/harper-fabric.d.ts +2 -2
- package/dist/configs/vitest/harper-fabric.d.ts.map +1 -1
- package/dist/configs/vitest/harper-fabric.js +4 -2
- package/dist/configs/vitest/harper-fabric.js.map +1 -1
- package/dist/configs/vitest/nestjs.d.ts +2 -2
- package/dist/configs/vitest/nestjs.d.ts.map +1 -1
- package/dist/configs/vitest/nestjs.js +4 -2
- package/dist/configs/vitest/nestjs.js.map +1 -1
- package/dist/configs/vitest/phaser.d.ts +2 -2
- package/dist/configs/vitest/phaser.d.ts.map +1 -1
- package/dist/configs/vitest/phaser.js +4 -2
- package/dist/configs/vitest/phaser.js.map +1 -1
- package/dist/configs/vitest/scratch-global-setup.d.ts +72 -0
- package/dist/configs/vitest/scratch-global-setup.d.ts.map +1 -0
- package/dist/configs/vitest/scratch-global-setup.js +168 -0
- package/dist/configs/vitest/scratch-global-setup.js.map +1 -0
- package/dist/configs/vitest/scratch-setup.d.ts +12 -0
- package/dist/configs/vitest/scratch-setup.d.ts.map +1 -0
- package/dist/configs/vitest/scratch-setup.js +70 -0
- package/dist/configs/vitest/scratch-setup.js.map +1 -0
- package/dist/configs/vitest/scratch.d.ts +192 -0
- package/dist/configs/vitest/scratch.d.ts.map +1 -0
- package/dist/configs/vitest/scratch.js +301 -0
- package/dist/configs/vitest/scratch.js.map +1 -0
- package/dist/configs/vitest/typescript.d.ts +2 -2
- package/dist/configs/vitest/typescript.d.ts.map +1 -1
- package/dist/configs/vitest/typescript.js +4 -2
- package/dist/configs/vitest/typescript.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +19 -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 +23 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/migrations/ensure-seeded-gates.d.ts +47 -0
- package/dist/migrations/ensure-seeded-gates.d.ts.map +1 -0
- package/dist/migrations/ensure-seeded-gates.js +139 -0
- package/dist/migrations/ensure-seeded-gates.js.map +1 -0
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +3 -0
- package/dist/migrations/index.js.map +1 -1
- package/package.json +2 -2
- 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/typescript/copy-contents/.husky/pre-push +21 -0
- package/typescript/copy-overwrite/.prettierignore +1 -0
- package/typescript/package-lisa/package.lisa.json +1 -0
|
@@ -77,6 +77,7 @@ export const HOLLOW_RESPONSES = ["report", "wait", "block"];
|
|
|
77
77
|
// moment" from "moment misspelled".
|
|
78
78
|
const SESSION_START = "session-start";
|
|
79
79
|
const PRE_TOOL = "pre-tool";
|
|
80
|
+
const POST_TOOL = "post-tool";
|
|
80
81
|
const COMMIT = "commit";
|
|
81
82
|
const PUSH = "push";
|
|
82
83
|
const PULL_REQUEST = "pull-request";
|
|
@@ -84,8 +85,37 @@ const PRE_DEPLOY = "pre-deploy";
|
|
|
84
85
|
const POST_DEPLOY = "post-deploy";
|
|
85
86
|
const CONTINUOUS = "continuous";
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* The agent tool boundary is TWO moments, and the difference is what a gate
|
|
90
|
+
* declared there is able to do.
|
|
91
|
+
*
|
|
92
|
+
* `pre-tool` runs BEFORE the write and can refuse it: the hook exits non-zero
|
|
93
|
+
* and the tool call never happens, so the file on disk is never touched. Only a
|
|
94
|
+
* check that can decide from the PROPOSED text belongs here — the shipped
|
|
95
|
+
* examples inspect the new content the tool is about to introduce.
|
|
96
|
+
*
|
|
97
|
+
* `post-tool` runs AFTER the write, against the file as it now exists. It
|
|
98
|
+
* cannot un-write anything; what it can do is fail loudly enough that the agent
|
|
99
|
+
* fixes it before moving on, which is why the shipped on-edit hooks exit 2.
|
|
100
|
+
*
|
|
101
|
+
* They are separated because collapsing them silently mis-declares every check
|
|
102
|
+
* on the larger side. Measured on `origin/main`: of the seven Lisa-shipped
|
|
103
|
+
* scripts on the `Write|Edit` boundary, five are registered `PostToolUse` and
|
|
104
|
+
* two `PreToolUse`. Calling all seven `pre-tool` would put five post-write
|
|
105
|
+
* checks behind a contract that promises the write can still be refused — the
|
|
106
|
+
* registry disagreeing with the repository, which the `structural-rules`
|
|
107
|
+
* correction below already establishes is worse than being merely permissive.
|
|
108
|
+
*/
|
|
109
|
+
const TOOL_MOMENTS = [PRE_TOOL, POST_TOOL];
|
|
110
|
+
|
|
87
111
|
/** Fixed moments. Two more families take an environment suffix. */
|
|
88
|
-
export const MOMENTS = [
|
|
112
|
+
export const MOMENTS = [
|
|
113
|
+
SESSION_START,
|
|
114
|
+
...TOOL_MOMENTS,
|
|
115
|
+
COMMIT,
|
|
116
|
+
PUSH,
|
|
117
|
+
PULL_REQUEST,
|
|
118
|
+
];
|
|
89
119
|
|
|
90
120
|
/** Moment families that take an `:<environment>` suffix. */
|
|
91
121
|
export const MOMENT_FAMILIES = [PRE_DEPLOY, POST_DEPLOY, CONTINUOUS];
|
|
@@ -162,6 +192,47 @@ const PUSH_ONWARD = [PUSH, PULL_REQUEST, PRE_DEPLOY, POST_DEPLOY];
|
|
|
162
192
|
const PR_ONWARD = [PULL_REQUEST, PRE_DEPLOY, POST_DEPLOY];
|
|
163
193
|
const DEPLOY_ONLY = [PRE_DEPLOY, POST_DEPLOY, CONTINUOUS];
|
|
164
194
|
const SESSION_ONWARD = [SESSION_START, ...COMMIT_ONWARD];
|
|
195
|
+
/**
|
|
196
|
+
* Legal from the moment an agent finishes writing a file, onward.
|
|
197
|
+
*
|
|
198
|
+
* Deliberately starts at `post-tool` and not at `pre-tool`: every gate that
|
|
199
|
+
* uses this list is proved by reading a file, and there is no file to read
|
|
200
|
+
* until the write has happened. A `pre-tool` check has only the proposed text.
|
|
201
|
+
*/
|
|
202
|
+
const EDIT_ONWARD = [POST_TOOL, ...COMMIT_ONWARD];
|
|
203
|
+
/**
|
|
204
|
+
* Legal from the moment a write is PROPOSED, onward.
|
|
205
|
+
*
|
|
206
|
+
* For properties provable from the text alone, which is what makes them
|
|
207
|
+
* refusable: the hook reads what the tool is about to introduce and can decline
|
|
208
|
+
* it. They stay legal at the later moments because the same property is
|
|
209
|
+
* provable against a diff once the write has landed — a project that would
|
|
210
|
+
* rather be told at commit than blocked mid-edit declares it there instead.
|
|
211
|
+
*/
|
|
212
|
+
const PRE_TOOL_ONWARD = [PRE_TOOL, ...COMMIT_ONWARD];
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The two gate ids named in more than one shipped table, and the task that
|
|
216
|
+
* takes the dependency audit over.
|
|
217
|
+
*
|
|
218
|
+
* Written as identifiers only where the id is REPEATED — the registry's own
|
|
219
|
+
* keys and the rest of `QUALITY_JOB_GATES` stay literal, because those tables
|
|
220
|
+
* are read as data by a consumer and an identifier is one indirection more than
|
|
221
|
+
* a reader of them needs.
|
|
222
|
+
*/
|
|
223
|
+
const CODE_STYLE = "code-style";
|
|
224
|
+
const DEPENDENCY_VULNERABILITY = "dependency-vulnerability";
|
|
225
|
+
const STRUCTURAL_RULES = "structural-rules";
|
|
226
|
+
const FORMAT_CONFORMANCE = "format-conformance";
|
|
227
|
+
|
|
228
|
+
/** The task a project would name to take the dependency audit over. */
|
|
229
|
+
const SECURITY_AUDIT_TASK = "security:audit";
|
|
230
|
+
|
|
231
|
+
/** The script every npm stack ships for dead-code detection. */
|
|
232
|
+
const KNIP_CHECK_TASK = "knip:check";
|
|
233
|
+
|
|
234
|
+
/** The older, unnamespaced fallback the same stacks still carry. */
|
|
235
|
+
const KNIP = "knip";
|
|
165
236
|
|
|
166
237
|
/**
|
|
167
238
|
* Lisa's canonical gates.
|
|
@@ -222,7 +293,12 @@ export const REGISTRY = Object.freeze({
|
|
|
222
293
|
label: "🧹 Lint",
|
|
223
294
|
summary: "Code conforms to the project's lint rules.",
|
|
224
295
|
task: "lint",
|
|
225
|
-
|
|
296
|
+
// Edit-legal on the same evidence that moved `structural-rules` to
|
|
297
|
+
// commit-onward: `lint-on-edit.sh` (TypeScript) and `rubocop-on-edit.sh`
|
|
298
|
+
// (Ruby) already lint every agent write, and both exit 2 on a finding.
|
|
299
|
+
// That enforcement happens on the highest-frequency surface Lisa owns, and
|
|
300
|
+
// until now no declaration could reach it.
|
|
301
|
+
moments: EDIT_ONWARD,
|
|
226
302
|
mayRewrite: true,
|
|
227
303
|
},
|
|
228
304
|
"code-style-slow": {
|
|
@@ -235,7 +311,12 @@ export const REGISTRY = Object.freeze({
|
|
|
235
311
|
label: "📐 Check Formatting",
|
|
236
312
|
summary: "Files match the project's formatter.",
|
|
237
313
|
task: "format:check",
|
|
238
|
-
|
|
314
|
+
// Edit-legal: `format-on-edit.sh` runs the formatter on every agent write.
|
|
315
|
+
// `rubocop-on-edit.sh` proves this property too — its own header says
|
|
316
|
+
// "RuboCop serves as both formatter and linter" — so one Ruby invocation
|
|
317
|
+
// stands for both this gate and `code-style`, and may stand down only when
|
|
318
|
+
// BOTH are covered.
|
|
319
|
+
moments: EDIT_ONWARD,
|
|
239
320
|
mayRewrite: true,
|
|
240
321
|
},
|
|
241
322
|
"type-correctness": {
|
|
@@ -444,14 +525,51 @@ export const REGISTRY = Object.freeze({
|
|
|
444
525
|
// demonstrably happens unrepresentable in config — a registry that
|
|
445
526
|
// disagrees with the repository is worse than one that is merely
|
|
446
527
|
// permissive, because it silently discards a real gate.
|
|
447
|
-
|
|
528
|
+
//
|
|
529
|
+
// Corrected once more, one moment earlier, on the identical argument:
|
|
530
|
+
// `sg-scan-on-edit.sh` runs `ast-grep scan` on every agent write, in both
|
|
531
|
+
// the TypeScript and Ruby stacks, and exits 2 on a finding. The same gate,
|
|
532
|
+
// the same reasoning, the same repository-versus-registry disagreement.
|
|
533
|
+
moments: EDIT_ONWARD,
|
|
448
534
|
work: "rules loaded",
|
|
449
535
|
},
|
|
536
|
+
"suppression-residue": {
|
|
537
|
+
label: "🚫 Suppression Residue",
|
|
538
|
+
summary:
|
|
539
|
+
"No new directive silencing the linter, type checker, or formatter.",
|
|
540
|
+
task: "check:suppressions",
|
|
541
|
+
// The one shipped prover is a genuine `PreToolUse` hook — it inspects the
|
|
542
|
+
// text the tool is about to write and exits 2, so the suppression never
|
|
543
|
+
// reaches the file. Named for the residue rather than for any single
|
|
544
|
+
// directive: the linter, type-checker and formatter each spell theirs
|
|
545
|
+
// differently, in every language Lisa supports, and a gate id naming one
|
|
546
|
+
// spelling would be a vendor id in the sense the registry header forbids.
|
|
547
|
+
declareOnly:
|
|
548
|
+
"No npm stack ships a script for this, and the exception is not a gap waiting on one. The prover Lisa ships is an agent hook that reads the text the tool is about to write and refuses it, which no `npm run` invocation can do — a script can only report on a suppression already in the file. Declaring this gate at a later moment means pointing `run:` at your own check.",
|
|
549
|
+
moments: PRE_TOOL_ONWARD,
|
|
550
|
+
work: "files inspected",
|
|
551
|
+
},
|
|
552
|
+
"migration-provenance": {
|
|
553
|
+
label: "🗃️ Migration Provenance",
|
|
554
|
+
summary:
|
|
555
|
+
"Schema migrations are generated from the model, not hand-written.",
|
|
556
|
+
task: "check:migrations",
|
|
557
|
+
// Also a `PreToolUse` refusal: the shipped hook blocks a write to a
|
|
558
|
+
// migration file outright, because a hand-edited migration drifts from the
|
|
559
|
+
// entity metadata it is supposed to describe and the drift is not visible
|
|
560
|
+
// until a deploy applies it. Stack-flavoured but registry-resident, the
|
|
561
|
+
// same way `e2e-native` is — the property is "this migration came from the
|
|
562
|
+
// model", which is true of any ORM that generates them.
|
|
563
|
+
declareOnly:
|
|
564
|
+
"No npm stack ships a prover. One stack ships `migration:generate`, but that is the generator this gate assumes was used, not a check that it was — and what a migration must be generated FROM is ORM-specific, so there is no one script to ship. The shipped prover is an agent hook that refuses the hand-edit outright; declaring this gate at a later moment means pointing `run:` at your own check.",
|
|
565
|
+
moments: PRE_TOOL_ONWARD,
|
|
566
|
+
work: "migrations checked",
|
|
567
|
+
},
|
|
450
568
|
"dead-code": {
|
|
451
569
|
label: "🗑️ Dead Code Detection",
|
|
452
570
|
summary: "No unused exports or dependencies.",
|
|
453
571
|
task: "check:dead-code",
|
|
454
|
-
shippedAs:
|
|
572
|
+
shippedAs: KNIP_CHECK_TASK,
|
|
455
573
|
declareOnly:
|
|
456
574
|
"Every npm stack ships `knip:check`, which proves this. CI's fallback runs it directly (and falls back again to the older `knip`), so the property is proved today whether or not the concern-named script exists.",
|
|
457
575
|
moments: PUSH_ONWARD,
|
|
@@ -467,7 +585,7 @@ export const REGISTRY = Object.freeze({
|
|
|
467
585
|
"dependency-vulnerability": {
|
|
468
586
|
label: "🔒 Security Scan",
|
|
469
587
|
summary: "No known high or critical advisory in shipped dependencies.",
|
|
470
|
-
task:
|
|
588
|
+
task: SECURITY_AUDIT_TASK,
|
|
471
589
|
declareOnly:
|
|
472
590
|
"The prover is the package manager's own `audit`, which CI invokes natively alongside an exclusion list. There is no script form to ship.",
|
|
473
591
|
// Continuous matters most here: a CVE published today makes yesterday's
|
|
@@ -596,8 +714,6 @@ export const REGISTRY = Object.freeze({
|
|
|
596
714
|
label: "🩹 Conflict Markers",
|
|
597
715
|
summary: "No leftover merge-conflict markers in tracked files.",
|
|
598
716
|
task: "check:conflict-markers",
|
|
599
|
-
declareOnly:
|
|
600
|
-
"A prover exists in Lisa's own `scripts/` and is not shipped to consumers. Exposing it is separate work; until then, point `run:` at your own.",
|
|
601
717
|
moments: COMMIT_ONWARD,
|
|
602
718
|
},
|
|
603
719
|
"version-duplication": {
|
|
@@ -647,7 +763,7 @@ export const REGISTRY = Object.freeze({
|
|
|
647
763
|
* the two disagree, which is what keeps a static copy true.
|
|
648
764
|
*/
|
|
649
765
|
export const QUALITY_JOB_GATES = Object.freeze({
|
|
650
|
-
lint:
|
|
766
|
+
lint: CODE_STYLE,
|
|
651
767
|
lint_slow: "code-style-slow",
|
|
652
768
|
typecheck: "type-correctness",
|
|
653
769
|
verification_coverage: "coverage-adequacy",
|
|
@@ -660,7 +776,7 @@ export const QUALITY_JOB_GATES = Object.freeze({
|
|
|
660
776
|
// the question this table answers is "which gate governs this job", and the
|
|
661
777
|
// answer does not depend on which file the job is written in.
|
|
662
778
|
playwright_e2e_aggregate: "e2e-browser",
|
|
663
|
-
format:
|
|
779
|
+
format: FORMAT_CONFORMANCE,
|
|
664
780
|
build: "build-integrity",
|
|
665
781
|
work_item_traceability: "traceability",
|
|
666
782
|
performance_budget: "performance-budget",
|
|
@@ -668,8 +784,9 @@ export const QUALITY_JOB_GATES = Object.freeze({
|
|
|
668
784
|
environment_reset: "environment-reset",
|
|
669
785
|
environment_reseed: "environment-reseed",
|
|
670
786
|
dead_code: "dead-code",
|
|
671
|
-
|
|
672
|
-
|
|
787
|
+
conflict_markers: "conflict-residue",
|
|
788
|
+
sg_scan: STRUCTURAL_RULES,
|
|
789
|
+
npm_security_scan: DEPENDENCY_VULNERABILITY,
|
|
673
790
|
threshold_ratchet: "threshold-monotonicity",
|
|
674
791
|
e2e_coverage: "journey-coverage",
|
|
675
792
|
state_classification: "state-classification",
|
|
@@ -733,6 +850,744 @@ export const UNGATED_QUALITY_JOBS = Object.freeze({
|
|
|
733
850
|
}),
|
|
734
851
|
});
|
|
735
852
|
|
|
853
|
+
/**
|
|
854
|
+
* How a hardcoded invocation relates to the gate façade.
|
|
855
|
+
*
|
|
856
|
+
* Two classes, because they fail differently and are fixed differently. A
|
|
857
|
+
* `consults-then-falls-back` step resolves the declaration FIRST and runs a
|
|
858
|
+
* written-in command only when nothing resolves, so a project can take it over
|
|
859
|
+
* by declaring the gate. A `never-consults` step has no config branch at all:
|
|
860
|
+
* the tool is written into the script, and there is nothing a project could
|
|
861
|
+
* declare that would replace it.
|
|
862
|
+
*/
|
|
863
|
+
export const CONSULTS_THEN_FALLS_BACK = "consults-then-falls-back";
|
|
864
|
+
|
|
865
|
+
/** A step with no config branch: the tool is written into the script. */
|
|
866
|
+
export const NEVER_CONSULTS = "never-consults";
|
|
867
|
+
|
|
868
|
+
/** Both classes, for validation and for reporting. */
|
|
869
|
+
export const FACADE_CLASSES = Object.freeze([
|
|
870
|
+
CONSULTS_THEN_FALLS_BACK,
|
|
871
|
+
NEVER_CONSULTS,
|
|
872
|
+
]);
|
|
873
|
+
|
|
874
|
+
/** The reusable workflow most façade jobs live in. */
|
|
875
|
+
const QUALITY_WORKFLOW = ".github/workflows/quality.yml";
|
|
876
|
+
|
|
877
|
+
/** The reusable workflow the browser suite moved to. */
|
|
878
|
+
const PLAYWRIGHT_WORKFLOW = ".github/workflows/playwright-e2e.yml";
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* The shipped pre-push hook, as the template that installs it.
|
|
882
|
+
*
|
|
883
|
+
* The repository's own `.husky/pre-push` is a copy of this with the resolver
|
|
884
|
+
* paths localised; naming the TEMPLATE is what makes the entry mean the same
|
|
885
|
+
* thing in a consumer, which has the installed copy and not this one.
|
|
886
|
+
*/
|
|
887
|
+
const PRE_PUSH_HOOK = "typescript/copy-contents/.husky/pre-push";
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* What each façade job runs when nothing resolves, and under which step names.
|
|
891
|
+
*
|
|
892
|
+
* Keyed by job id so the gate comes from `QUALITY_JOB_GATES` rather than being
|
|
893
|
+
* written twice. `steps` are the exact step names carrying
|
|
894
|
+
* `if: steps.gate.outputs.configured == 'false'` — named rather than counted,
|
|
895
|
+
* because a report that cannot say WHICH built-in ran is not much better than
|
|
896
|
+
* no report, and because the inventory test can then fail on a renamed step
|
|
897
|
+
* instead of silently checking nothing.
|
|
898
|
+
*/
|
|
899
|
+
const QUALITY_FALLBACKS = Object.freeze({
|
|
900
|
+
lint: {
|
|
901
|
+
command: "<package-manager> run lint",
|
|
902
|
+
steps: ["🦀 Verify oxlint is installed", "🧹 Run linter (oxlint + eslint)"],
|
|
903
|
+
},
|
|
904
|
+
lint_slow: {
|
|
905
|
+
command: "<package-manager> run lint:slow",
|
|
906
|
+
steps: ["🐢 Run slow lint rules"],
|
|
907
|
+
},
|
|
908
|
+
typecheck: {
|
|
909
|
+
command: "<package-manager> run typecheck",
|
|
910
|
+
steps: ["🔍 Run type check"],
|
|
911
|
+
},
|
|
912
|
+
build: {
|
|
913
|
+
command: "<package-manager> run build",
|
|
914
|
+
steps: ["🏗️ Build project"],
|
|
915
|
+
},
|
|
916
|
+
format: {
|
|
917
|
+
command: "<package-manager> run format:check",
|
|
918
|
+
steps: ["📐 Check formatting"],
|
|
919
|
+
},
|
|
920
|
+
test_unit: {
|
|
921
|
+
// `test:cov`, not `test:unit`: the CI fallback proves the suite AND the
|
|
922
|
+
// coverage thresholds in one run, so seeding the registry default would
|
|
923
|
+
// stop enforcing coverage here without changing a single line of config.
|
|
924
|
+
command: "<package-manager> run test:cov",
|
|
925
|
+
seedRun: ["test:cov"],
|
|
926
|
+
steps: [
|
|
927
|
+
"🧪 Run unit tests with coverage",
|
|
928
|
+
"⏭️ Skip unit tests (no test:unit script)",
|
|
929
|
+
],
|
|
930
|
+
},
|
|
931
|
+
test_integration: {
|
|
932
|
+
command: "<package-manager> run test:integration",
|
|
933
|
+
steps: [
|
|
934
|
+
"🧪 Run integration tests",
|
|
935
|
+
"⏭️ Skip integration tests (no test:integration script)",
|
|
936
|
+
],
|
|
937
|
+
},
|
|
938
|
+
test_mutation: {
|
|
939
|
+
command: "<package-manager> run test:mutation",
|
|
940
|
+
steps: [
|
|
941
|
+
"🧬 Run mutation-testing gate (diff-only, self-skips when disabled)",
|
|
942
|
+
],
|
|
943
|
+
},
|
|
944
|
+
performance_budget: {
|
|
945
|
+
command: "<package-manager> run lighthouse:check",
|
|
946
|
+
seedRun: ["lighthouse:check"],
|
|
947
|
+
steps: [
|
|
948
|
+
"⚡ Run the performance budget (lighthouse:check)",
|
|
949
|
+
"⏭️ Skip the performance budget (no export:web script)",
|
|
950
|
+
],
|
|
951
|
+
},
|
|
952
|
+
test_node_suites: {
|
|
953
|
+
command: "node <lisa>/scripts/lisa-test-node.mjs",
|
|
954
|
+
seedRun: ["test:node"],
|
|
955
|
+
steps: ["🧪 Run .mjs suites (lisa-test-node)"],
|
|
956
|
+
},
|
|
957
|
+
environment_reset: {
|
|
958
|
+
// Lisa ships no implementation behind the environment façade, so the
|
|
959
|
+
// fallback announces the absence rather than substituting for it. Nothing
|
|
960
|
+
// to seed: the declaration IS the adapter, and there is no task to name.
|
|
961
|
+
command: "(none — the fallback announces the absent adapter)",
|
|
962
|
+
seedRun: [],
|
|
963
|
+
steps: ["♻️ No environment reset adapter declared"],
|
|
964
|
+
},
|
|
965
|
+
environment_reseed: {
|
|
966
|
+
command: "(none — the fallback announces the absent adapter)",
|
|
967
|
+
seedRun: [],
|
|
968
|
+
steps: ["🌱 No environment reseed adapter declared"],
|
|
969
|
+
},
|
|
970
|
+
npm_security_scan: {
|
|
971
|
+
command: "<package-manager> audit, filtered through audit.ignore*.json",
|
|
972
|
+
seedRun: [SECURITY_AUDIT_TASK],
|
|
973
|
+
steps: ["📋 Load audit exclusions", "🔒 Run security audit"],
|
|
974
|
+
},
|
|
975
|
+
dead_code: {
|
|
976
|
+
command: "<package-manager> run knip:check",
|
|
977
|
+
seedRun: [KNIP_CHECK_TASK, KNIP],
|
|
978
|
+
steps: ["🗑️ Run dead code detection (knip)"],
|
|
979
|
+
},
|
|
980
|
+
conflict_markers: {
|
|
981
|
+
// The built-in resolves the shipped `check-conflict-markers.mjs` from
|
|
982
|
+
// whichever of three locations the project has it in, so the command is a
|
|
983
|
+
// resolution rather than a task. Nothing is seeded: a project naming one
|
|
984
|
+
// task would replace a probe that also decides WHICH copy to run.
|
|
985
|
+
command: "node <lisa>/scripts/check-conflict-markers.mjs",
|
|
986
|
+
seedRun: [],
|
|
987
|
+
steps: ["🩹 Check for leftover conflict markers"],
|
|
988
|
+
},
|
|
989
|
+
sg_scan: {
|
|
990
|
+
// Measured, not assumed: the whole ast-grep pipeline hangs off the
|
|
991
|
+
// `🔍 Check for sgconfig.yml` discovery step, so declaring the gate takes
|
|
992
|
+
// the scan AND the rule tests with it. A one-task declaration would
|
|
993
|
+
// therefore narrow what is proved while reading like a takeover, so
|
|
994
|
+
// nothing is seeded here — the gap stays reported instead.
|
|
995
|
+
command: "ast-grep scan --config sgconfig.yml, plus the rule tests",
|
|
996
|
+
seedRun: [],
|
|
997
|
+
steps: ["🔍 Check for sgconfig.yml", "⏭️ AST Grep Skipped (no config)"],
|
|
998
|
+
},
|
|
999
|
+
verification_coverage: {
|
|
1000
|
+
// A spec-delta check over the diff, not a task the project could name, so
|
|
1001
|
+
// no seeded declaration reproduces it. An empty `seedRun` is the explicit
|
|
1002
|
+
// "nothing to seed here" — distinct from `null`, which means "the registry
|
|
1003
|
+
// default is already right".
|
|
1004
|
+
command: "(bespoke — requires a verification spec delta on feat/fix)",
|
|
1005
|
+
seedRun: [],
|
|
1006
|
+
steps: ["✅ Require a verification (e2e) spec delta on feat/fix"],
|
|
1007
|
+
},
|
|
1008
|
+
work_item_traceability: {
|
|
1009
|
+
command: "node <lisa>/scripts/lisa-work-item.mjs validate-pr",
|
|
1010
|
+
seedRun: ["check:work-item"],
|
|
1011
|
+
steps: ["🔗 Validate Work-Item traceability"],
|
|
1012
|
+
},
|
|
1013
|
+
threshold_ratchet: {
|
|
1014
|
+
// The built-in resolves the shipped `check-threshold-ratchet.mjs` and
|
|
1015
|
+
// diffs against the merge-base, branching on whether the head ref exists
|
|
1016
|
+
// on the remote. Nothing is seeded: the pair of invocations is the check,
|
|
1017
|
+
// and a one-task declaration would silently drop the head-ref arm, so the
|
|
1018
|
+
// gap stays reported rather than declared away.
|
|
1019
|
+
command:
|
|
1020
|
+
"node <lisa>/scripts/check-threshold-ratchet.mjs --base origin/<base> [--head origin/<head>]",
|
|
1021
|
+
seedRun: [],
|
|
1022
|
+
steps: ["📐 Compare thresholds against merge-base"],
|
|
1023
|
+
},
|
|
1024
|
+
e2e_coverage: {
|
|
1025
|
+
// A repository-local script the job PROBES for before running, and the
|
|
1026
|
+
// absent branch is a warning rather than a failure. A one-task declaration
|
|
1027
|
+
// would drop the probe, so nothing is seeded and the gap stays reported.
|
|
1028
|
+
command: "node scripts/check-e2e-coverage.mjs (when the script exists)",
|
|
1029
|
+
seedRun: [],
|
|
1030
|
+
steps: [
|
|
1031
|
+
"🧭 Require e2e route/screen coverage thresholds",
|
|
1032
|
+
"⏭️ Skip e2e coverage (no check-e2e-coverage.mjs script)",
|
|
1033
|
+
],
|
|
1034
|
+
},
|
|
1035
|
+
state_classification: {
|
|
1036
|
+
command:
|
|
1037
|
+
"node scripts/check-state-classification.mjs (when the script exists)",
|
|
1038
|
+
seedRun: [],
|
|
1039
|
+
steps: [
|
|
1040
|
+
"🧬 Require every persistent entity to carry a reset policy",
|
|
1041
|
+
"⏭️ Skip state classification (no check-state-classification.mjs script)",
|
|
1042
|
+
],
|
|
1043
|
+
},
|
|
1044
|
+
floor_collisions: {
|
|
1045
|
+
// Two candidate paths, and Lisa's own repository ships the script as a
|
|
1046
|
+
// template rather than installing it — so the built-in chooses between
|
|
1047
|
+
// them at run time and a single named task cannot reproduce the choice.
|
|
1048
|
+
command: "node <lisa>/scripts/lisa-floor-collisions.mjs",
|
|
1049
|
+
seedRun: [],
|
|
1050
|
+
steps: ["🧱 Check for collapsible security floors"],
|
|
1051
|
+
},
|
|
1052
|
+
secret_scanning: {
|
|
1053
|
+
// A third-party scanner behind a secret, not a task: with no
|
|
1054
|
+
// `GITGUARDIAN_API_KEY` the job takes its skip branch and reports success
|
|
1055
|
+
// having scanned nothing. That is precisely the shape this inventory
|
|
1056
|
+
// exists to make visible, and nothing a project could declare replaces it.
|
|
1057
|
+
command: "ggshield secret scan ci (when GITGUARDIAN_API_KEY is set)",
|
|
1058
|
+
seedRun: [],
|
|
1059
|
+
steps: [
|
|
1060
|
+
"🔍 Check for GitGuardian API key",
|
|
1061
|
+
"🔐 GitGuardian scan",
|
|
1062
|
+
"🔐 GitGuardian Scan Skipped",
|
|
1063
|
+
],
|
|
1064
|
+
},
|
|
1065
|
+
license_compliance: {
|
|
1066
|
+
command: "fossas/fossa-action (when FOSSA_API_KEY is set)",
|
|
1067
|
+
seedRun: [],
|
|
1068
|
+
steps: [
|
|
1069
|
+
"🔍 Check for FOSSA API key",
|
|
1070
|
+
"📜 Run FOSSA license scan",
|
|
1071
|
+
"📜 FOSSA Scan Skipped",
|
|
1072
|
+
],
|
|
1073
|
+
},
|
|
1074
|
+
maestro_e2e: {
|
|
1075
|
+
// Three secrets and inputs gate this one, each with its own skip branch,
|
|
1076
|
+
// so the job has four ways to report green having run no test at all.
|
|
1077
|
+
command:
|
|
1078
|
+
"mobile-dev-inc/action-maestro-cloud (when the key, project id and app file are all present)",
|
|
1079
|
+
seedRun: [],
|
|
1080
|
+
steps: [
|
|
1081
|
+
"🔍 Check for Maestro API key",
|
|
1082
|
+
"🔍 Check for project ID",
|
|
1083
|
+
"🔍 Check for app file",
|
|
1084
|
+
"📱 Run Maestro Cloud tests",
|
|
1085
|
+
"📱 Maestro Tests Skipped (no API key)",
|
|
1086
|
+
"📱 Maestro Tests Skipped (no project ID)",
|
|
1087
|
+
"📱 Maestro Tests Skipped (no app file)",
|
|
1088
|
+
],
|
|
1089
|
+
},
|
|
1090
|
+
sonarcloud: {
|
|
1091
|
+
// The scan and the verdict step are a pair — the action's own outcome is
|
|
1092
|
+
// read by a separate step that decides whether to fail — so one declared
|
|
1093
|
+
// task would silently drop the half that can redden the job.
|
|
1094
|
+
command:
|
|
1095
|
+
"SonarSource/sonarqube-scan-action plus its result check (when SONAR_TOKEN is set)",
|
|
1096
|
+
seedRun: [],
|
|
1097
|
+
steps: [
|
|
1098
|
+
"🔍 Check for SonarCloud token",
|
|
1099
|
+
"📊 SonarCloud Scan",
|
|
1100
|
+
"🔍 Validate SonarCloud results",
|
|
1101
|
+
"📊 SonarCloud Scan Skipped",
|
|
1102
|
+
],
|
|
1103
|
+
},
|
|
1104
|
+
playwright_e2e_aggregate: {
|
|
1105
|
+
file: PLAYWRIGHT_WORKFLOW,
|
|
1106
|
+
// Not seedable for the same reason as `sg_scan`, and with more at stake:
|
|
1107
|
+
// Lisa's implementation of this gate is a sharded matrix, a blob merge AND
|
|
1108
|
+
// the verdict step that stops the merge reporting green over failing
|
|
1109
|
+
// shards. One task replaces all three.
|
|
1110
|
+
command: "(bespoke — a sharded Playwright matrix plus its blob merge)",
|
|
1111
|
+
seedRun: [],
|
|
1112
|
+
steps: [
|
|
1113
|
+
"📥 Download all shard blob reports",
|
|
1114
|
+
"🎭 Merge blob reports into HTML",
|
|
1115
|
+
"📤 Upload merged Playwright report",
|
|
1116
|
+
"🎭 Playwright aggregator skipped (no config)",
|
|
1117
|
+
"🚨 Fail if any Playwright shard failed",
|
|
1118
|
+
],
|
|
1119
|
+
},
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* The façade jobs, as inventory entries.
|
|
1124
|
+
*
|
|
1125
|
+
* A function rather than a literal so the gate id comes from
|
|
1126
|
+
* `QUALITY_JOB_GATES` — the same table `lisa doctor` and a consumer migrating
|
|
1127
|
+
* off `skip_jobs` read — instead of being spelled out a second time here where
|
|
1128
|
+
* the two copies could disagree.
|
|
1129
|
+
* @returns {object[]} One frozen entry per façade job.
|
|
1130
|
+
*/
|
|
1131
|
+
function qualityInvocations() {
|
|
1132
|
+
return Object.entries(QUALITY_FALLBACKS).map(([job, entry]) => {
|
|
1133
|
+
const gate = QUALITY_JOB_GATES[job];
|
|
1134
|
+
if (gate === undefined) {
|
|
1135
|
+
throw new Error(
|
|
1136
|
+
`${job} has a fallback recorded but no gate in QUALITY_JOB_GATES.`
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
const artifact = entry.file ?? QUALITY_WORKFLOW;
|
|
1140
|
+
return Object.freeze({
|
|
1141
|
+
gate,
|
|
1142
|
+
// The workflow's own default. A caller passing `moment:` runs the same
|
|
1143
|
+
// jobs against a different declaration set; the entry names where the
|
|
1144
|
+
// fallback lives by default, which is what a report at that moment needs.
|
|
1145
|
+
moment: PULL_REQUEST,
|
|
1146
|
+
surface:
|
|
1147
|
+
artifact === PLAYWRIGHT_WORKFLOW
|
|
1148
|
+
? "playwright-workflow"
|
|
1149
|
+
: "quality-workflow",
|
|
1150
|
+
artifact,
|
|
1151
|
+
job,
|
|
1152
|
+
command: entry.command,
|
|
1153
|
+
steps: Object.freeze([...entry.steps]),
|
|
1154
|
+
seedRun:
|
|
1155
|
+
entry.seedRun === undefined ? null : Object.freeze([...entry.seedRun]),
|
|
1156
|
+
facade: CONSULTS_THEN_FALLS_BACK,
|
|
1157
|
+
});
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* One pre-push entry, with the fields every inventory entry carries.
|
|
1163
|
+
* @param {string} gate Registry gate id.
|
|
1164
|
+
* @param {string} command What the built-in else-branch runs.
|
|
1165
|
+
* @param {string[]|null} seedRun Candidate task names, or null for the registry default.
|
|
1166
|
+
* @returns {object} A frozen inventory entry.
|
|
1167
|
+
*/
|
|
1168
|
+
function prePushInvocation(gate, command, seedRun) {
|
|
1169
|
+
return Object.freeze({
|
|
1170
|
+
gate,
|
|
1171
|
+
moment: PUSH,
|
|
1172
|
+
surface: "pre-push-hook",
|
|
1173
|
+
artifact: PRE_PUSH_HOOK,
|
|
1174
|
+
job: null,
|
|
1175
|
+
command,
|
|
1176
|
+
steps: Object.freeze([]),
|
|
1177
|
+
seedRun: seedRun === null ? null : Object.freeze([...seedRun]),
|
|
1178
|
+
facade: CONSULTS_THEN_FALLS_BACK,
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* One on-edit hook entry.
|
|
1184
|
+
* @param {string} gate Registry gate id the tool proves.
|
|
1185
|
+
* @param {string} artifact Repository-relative path to the source hook.
|
|
1186
|
+
* @param {string} command The tool written into the script.
|
|
1187
|
+
* @returns {object} A frozen inventory entry.
|
|
1188
|
+
*/
|
|
1189
|
+
function onEditInvocation(gate, artifact, command) {
|
|
1190
|
+
return Object.freeze({
|
|
1191
|
+
gate,
|
|
1192
|
+
// PLACEHOLDER, AND KNOWN TO BE WRONG. These scripts are registered as
|
|
1193
|
+
// `PostToolUse` — measured from the plugin manifests, and pinned by
|
|
1194
|
+
// `hookEvent` below — but the registry has no `post-tool` moment to record
|
|
1195
|
+
// that with, so this names the nearest edit-time moment `MOMENTS` does
|
|
1196
|
+
// have. Recording the accurate value is blocked on the registry gaining
|
|
1197
|
+
// `post-tool`; recording it SILENTLY as `pre-tool` is what this comment and
|
|
1198
|
+
// `hookEvent` exist to prevent. Nothing depends on the distinction today —
|
|
1199
|
+
// no gate lists either moment, so both are equally undeclarable — but a
|
|
1200
|
+
// reader must not take this field for a measurement.
|
|
1201
|
+
moment: PRE_TOOL,
|
|
1202
|
+
// The event the shipped manifest actually registers. Derived and pinned by
|
|
1203
|
+
// the inventory test against `plugin.json`, so this cannot drift the way
|
|
1204
|
+
// `moment` did: the first version of this table said these fire before the
|
|
1205
|
+
// edit, and nothing contradicted it.
|
|
1206
|
+
hookEvent: "PostToolUse",
|
|
1207
|
+
surface: "on-edit-hook",
|
|
1208
|
+
artifact,
|
|
1209
|
+
job: null,
|
|
1210
|
+
command,
|
|
1211
|
+
steps: Object.freeze([]),
|
|
1212
|
+
// Nothing to seed: `pre-tool` is a moment no registry gate lists, so no
|
|
1213
|
+
// declaration at it is legal, and `validate` would refuse one.
|
|
1214
|
+
seedRun: Object.freeze([]),
|
|
1215
|
+
facade: NEVER_CONSULTS,
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* Every invocation Lisa ships with a command written into the artifact rather
|
|
1221
|
+
* than resolved from the project's declaration, and the gate that should
|
|
1222
|
+
* govern it.
|
|
1223
|
+
*
|
|
1224
|
+
* WHY THIS IS DATA AND NOT PROSE. "Which properties is this repository proving
|
|
1225
|
+
* with a command nothing declared" has to be answerable inside a CONSUMER, at
|
|
1226
|
+
* the moment the unconfigured step runs — and a consumer holds no copy of
|
|
1227
|
+
* `quality.yml` (it calls the reusable workflow by ref) and no copy of Lisa's
|
|
1228
|
+
* tests. Written as documentation the answer would be readable only here;
|
|
1229
|
+
* written as a table beside the registry it is readable by
|
|
1230
|
+
* `lisa-gates.mjs unconfigured`, which is what the pre-push hook and every
|
|
1231
|
+
* gated CI job call to say what they just ran ungoverned.
|
|
1232
|
+
*
|
|
1233
|
+
* `command` is DESCRIPTIVE, not executable. It names what the artifact runs so
|
|
1234
|
+
* a report can print it; `<runner>` stands for the project's task runner,
|
|
1235
|
+
* `<package-manager>` for the caller's, `<lisa>` for wherever the installed
|
|
1236
|
+
* package resolved — all three are decided at run time.
|
|
1237
|
+
*
|
|
1238
|
+
* `seedRun` is what makes seeding safe. A seeded declaration must reproduce
|
|
1239
|
+
* today's invocation, and for several of these the registry's default task is
|
|
1240
|
+
* NOT what the artifact runs: the pre-push unit step runs `test:cov:unit`,
|
|
1241
|
+
* which proves the suite and the coverage thresholds together, where
|
|
1242
|
+
* `test-correctness` defaults to `test:unit`. Seeding the default there would
|
|
1243
|
+
* silently stop enforcing coverage at push while the configuration read
|
|
1244
|
+
* deliberate. Where `seedRun` is a non-empty array it lists candidate task
|
|
1245
|
+
* names in preference order; where it is `null` the registry default already
|
|
1246
|
+
* matches; where it is an EMPTY array nothing a project could name reproduces
|
|
1247
|
+
* the built-in, so `seedGates` declares nothing for it.
|
|
1248
|
+
*
|
|
1249
|
+
* Kept true by `tests/integration/hardcoded-invocation-inventory.test.ts`,
|
|
1250
|
+
* which derives the population from the shipped artifacts in BOTH directions:
|
|
1251
|
+
* an entry naming a step that is gone fails, and a façade job, a
|
|
1252
|
+
* `lisa_gate_covers` call, or an `-on-edit.sh` source script with no entry
|
|
1253
|
+
* fails too.
|
|
1254
|
+
*/
|
|
1255
|
+
export const HARDCODED_INVOCATIONS = Object.freeze([
|
|
1256
|
+
// ── Class A: the pre-push hook. Nine gated steps, nine written-in else
|
|
1257
|
+
// branches. Each resolves through `lisa-run-gates.mjs --moment=push` first
|
|
1258
|
+
// and stands down only against its own property.
|
|
1259
|
+
prePushInvocation(
|
|
1260
|
+
"traceability",
|
|
1261
|
+
"node <lisa>/scripts/lisa-work-item.mjs validate-push <remote>",
|
|
1262
|
+
["check:work-item:push"]
|
|
1263
|
+
),
|
|
1264
|
+
prePushInvocation("type-correctness", "<runner> typecheck", null),
|
|
1265
|
+
// Three transports behind one property — `bun audit --production --json`,
|
|
1266
|
+
// `npm audit --production --json`, `yarn audit --groups dependencies --json`
|
|
1267
|
+
// — chosen by which lockfile is present. None of them is a task the project
|
|
1268
|
+
// named, and `gates.runner` has no say in any of them.
|
|
1269
|
+
prePushInvocation(
|
|
1270
|
+
DEPENDENCY_VULNERABILITY,
|
|
1271
|
+
"bun|npm|yarn audit --json, filtered through audit.ignore*.json",
|
|
1272
|
+
[SECURITY_AUDIT_TASK]
|
|
1273
|
+
),
|
|
1274
|
+
prePushInvocation("code-style-slow", "<runner> lint:slow", null),
|
|
1275
|
+
prePushInvocation("dead-code", "<runner> knip:check", [
|
|
1276
|
+
KNIP_CHECK_TASK,
|
|
1277
|
+
KNIP,
|
|
1278
|
+
]),
|
|
1279
|
+
prePushInvocation("test-correctness", "<runner> test:cov:unit", [
|
|
1280
|
+
"test:cov:unit",
|
|
1281
|
+
"test:cov",
|
|
1282
|
+
]),
|
|
1283
|
+
// The SAME run proves both properties, which is why that step stands down
|
|
1284
|
+
// only when both are declared. Seeding one without the other leaves the
|
|
1285
|
+
// built-in running and proves the pair twice.
|
|
1286
|
+
prePushInvocation("coverage-adequacy", "<runner> test:cov:unit", [
|
|
1287
|
+
"test:cov:unit",
|
|
1288
|
+
"test:cov",
|
|
1289
|
+
]),
|
|
1290
|
+
prePushInvocation("test-integration", "<runner> test:integration", null),
|
|
1291
|
+
// The worst Class-A case, and the reason `declarable` is COMPUTED rather
|
|
1292
|
+
// than assumed: `test-meaningfulness` is PR-onward, so `push` is not a legal
|
|
1293
|
+
// moment to declare it at and `validate` refuses the declaration. There is
|
|
1294
|
+
// therefore no gates configuration that governs mutation testing at push;
|
|
1295
|
+
// the only lever is deleting the `test:mutation` script. Fixing that is not
|
|
1296
|
+
// this table's job — being unable to see it was.
|
|
1297
|
+
prePushInvocation("test-meaningfulness", "<runner> test:mutation", []),
|
|
1298
|
+
// ── Class A: the reusable workflows. One façade job per gate.
|
|
1299
|
+
...qualityInvocations(),
|
|
1300
|
+
// ── Class B: the agent tool hooks. No config branch of any kind — measured,
|
|
1301
|
+
// and pinned by the inventory test: none of these scripts mentions
|
|
1302
|
+
// `lisa_gate_covers`, `lisa-run-gates` or `lisa-gates`. What they resolve is
|
|
1303
|
+
// the RUNNER (`./node_modules/.bin/oxlint`, else `bunx`/`npx`), never the
|
|
1304
|
+
// TOOL. They also exit 2 — blocking the edit — when their written-in tool is
|
|
1305
|
+
// absent, so a project that lints correctly with something else has every
|
|
1306
|
+
// agent edit refused until it installs Lisa's choice.
|
|
1307
|
+
//
|
|
1308
|
+
// Edit time is the highest-frequency enforcement surface Lisa owns and the
|
|
1309
|
+
// one surface with no configurability at all. It is also the one where
|
|
1310
|
+
// standing down is safe: an on-edit hook is not a branch-protection context,
|
|
1311
|
+
// so a hook that declines to run manufactures no green required check.
|
|
1312
|
+
onEditInvocation(
|
|
1313
|
+
CODE_STYLE,
|
|
1314
|
+
"plugins/src/typescript/hooks/lint-on-edit.sh",
|
|
1315
|
+
"oxlint <edited-file>"
|
|
1316
|
+
),
|
|
1317
|
+
onEditInvocation(
|
|
1318
|
+
FORMAT_CONFORMANCE,
|
|
1319
|
+
"plugins/src/typescript/hooks/format-on-edit.sh",
|
|
1320
|
+
"prettier --write <edited-file>"
|
|
1321
|
+
),
|
|
1322
|
+
onEditInvocation(
|
|
1323
|
+
STRUCTURAL_RULES,
|
|
1324
|
+
"plugins/src/typescript/hooks/sg-scan-on-edit.sh",
|
|
1325
|
+
"ast-grep scan <edited-file>"
|
|
1326
|
+
),
|
|
1327
|
+
// RuboCop is BOTH halves, which is why this artifact appears twice. Its own
|
|
1328
|
+
// header calls it the "Lint-and-Format-on-Edit Hook" and it runs
|
|
1329
|
+
// `rubocop -a` — safe autocorrect — before checking for what is left. An
|
|
1330
|
+
// inventory recording only the lint half would report a formatter that runs
|
|
1331
|
+
// on every agent edit as not running at all.
|
|
1332
|
+
onEditInvocation(
|
|
1333
|
+
CODE_STYLE,
|
|
1334
|
+
"plugins/src/rails/hooks/rubocop-on-edit.sh",
|
|
1335
|
+
"rubocop -a --fail-level E <edited-file>"
|
|
1336
|
+
),
|
|
1337
|
+
onEditInvocation(
|
|
1338
|
+
FORMAT_CONFORMANCE,
|
|
1339
|
+
"plugins/src/rails/hooks/rubocop-on-edit.sh",
|
|
1340
|
+
"rubocop -a (safe autocorrect) <edited-file>"
|
|
1341
|
+
),
|
|
1342
|
+
onEditInvocation(
|
|
1343
|
+
STRUCTURAL_RULES,
|
|
1344
|
+
"plugins/src/rails/hooks/sg-scan-on-edit.sh",
|
|
1345
|
+
"ast-grep scan <edited-file>"
|
|
1346
|
+
),
|
|
1347
|
+
]);
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* Whether a gate may legally be declared at a moment.
|
|
1351
|
+
*
|
|
1352
|
+
* Computed from the registry rather than recorded on the entry, because the two
|
|
1353
|
+
* answers must never be able to disagree: an entry that claimed a moment was
|
|
1354
|
+
* declarable after the registry stopped listing it would send an operator to
|
|
1355
|
+
* write a declaration `validate` then refuses.
|
|
1356
|
+
* @param {string} gate Registry gate id.
|
|
1357
|
+
* @param {string} moment The moment in question.
|
|
1358
|
+
* @returns {boolean} True when a declaration at that moment is legal.
|
|
1359
|
+
*/
|
|
1360
|
+
export function isDeclarableAt(gate, moment) {
|
|
1361
|
+
const definition = REGISTRY[gate];
|
|
1362
|
+
if (definition === undefined) return false;
|
|
1363
|
+
return definition.moments.includes(momentFamily(moment));
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* The hardcoded invocations at one moment, optionally on one surface.
|
|
1368
|
+
* @param {object} options Filter.
|
|
1369
|
+
* @param {string} [options.moment] Only invocations at this moment.
|
|
1370
|
+
* @param {string} [options.surface] Only invocations on this surface.
|
|
1371
|
+
* @param {string} [options.gate] Only invocations for this gate.
|
|
1372
|
+
* @returns {object[]} Matching entries, in table order.
|
|
1373
|
+
*/
|
|
1374
|
+
export function hardcodedAt({ moment, surface, gate } = {}) {
|
|
1375
|
+
return HARDCODED_INVOCATIONS.filter(
|
|
1376
|
+
entry =>
|
|
1377
|
+
(moment === undefined || entry.moment === moment) &&
|
|
1378
|
+
(surface === undefined || entry.surface === surface) &&
|
|
1379
|
+
(gate === undefined || entry.gate === gate)
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* The properties proved at a moment by a command the project did not declare.
|
|
1385
|
+
*
|
|
1386
|
+
* A gate declared `off` is NOT reported: `off` is a declaration, and reporting
|
|
1387
|
+
* it as ungoverned would train an operator to ignore the report. A gate
|
|
1388
|
+
* declared `await` at this moment is also a declaration, but the built-in still
|
|
1389
|
+
* runs, so it IS reported — the project said "something else proves this" and
|
|
1390
|
+
* the written-in command ran anyway.
|
|
1391
|
+
* @param {object} options Resolution inputs.
|
|
1392
|
+
* @param {object} options.gates The gates block.
|
|
1393
|
+
* @param {string} options.moment The moment being proved.
|
|
1394
|
+
* @param {string} [options.surface] Restrict to one surface.
|
|
1395
|
+
* @param {string} [options.gate] Restrict to one gate id.
|
|
1396
|
+
* @returns {Array<{gate: string, label: string, command: string, surface: string, artifact: string, job: string|null, steps: string[], declarable: boolean, declaration: string|null, reason: string}>} One finding per ungoverned invocation.
|
|
1397
|
+
*/
|
|
1398
|
+
export function unconfiguredAt({ gates, moment, surface, gate }) {
|
|
1399
|
+
const declared = new Map(
|
|
1400
|
+
resolveMoment({ gates: gates ?? {}, moment, includeOff: true }).map(
|
|
1401
|
+
entry => [entry.id, entry]
|
|
1402
|
+
)
|
|
1403
|
+
);
|
|
1404
|
+
// The moment filters the INVENTORY only when the caller has not named a
|
|
1405
|
+
// gate. A workflow's cadence and the moment an entry records are different
|
|
1406
|
+
// facts — the browser suite's workflow defaults to `continuous:development`
|
|
1407
|
+
// while its entry records `pull-request` — and a caller asking about ONE gate
|
|
1408
|
+
// on ONE surface is asking "what does the built-in here run", which does not
|
|
1409
|
+
// depend on the cadence. Filtering by moment there produced no entry, hence
|
|
1410
|
+
// no finding, hence a report step that printed nothing at all: this control's
|
|
1411
|
+
// own failure mode, inside the control built to expose it.
|
|
1412
|
+
const scoped = hardcodedAt({
|
|
1413
|
+
moment: gate === undefined ? moment : undefined,
|
|
1414
|
+
surface,
|
|
1415
|
+
gate,
|
|
1416
|
+
});
|
|
1417
|
+
const findings = [];
|
|
1418
|
+
for (const entry of scoped) {
|
|
1419
|
+
const hit = declared.get(entry.gate);
|
|
1420
|
+
// WHICH DECLARATIONS ACTUALLY GOVERN. Three kinds do not, and treating any
|
|
1421
|
+
// of them as governance makes this report go quiet while the written-in
|
|
1422
|
+
// command still runs — a control reporting success having proved nothing,
|
|
1423
|
+
// which is the defect this whole subsystem exists to remove. Measured on
|
|
1424
|
+
// this branch before the guard: an ILLEGAL `code-style` declaration at the
|
|
1425
|
+
// on-edit moment silenced 2 of 5 findings, and an illegal
|
|
1426
|
+
// `test-meaningfulness` at push silenced 1 of 9 — both declarations that
|
|
1427
|
+
// `validate` refuses outright.
|
|
1428
|
+
//
|
|
1429
|
+
// * `await` proves nothing here, and the built-in ran anyway.
|
|
1430
|
+
// * A declaration at a moment the gate may not legally be declared at is
|
|
1431
|
+
// not a configuration, it is a config error. `validate` rejects it, so
|
|
1432
|
+
// it must not buy silence from a report either.
|
|
1433
|
+
// * A `never-consults` artifact reads no declaration at all, so NOTHING a
|
|
1434
|
+
// project writes takes it over. Suppressing on a declaration there
|
|
1435
|
+
// would report a takeover that cannot happen.
|
|
1436
|
+
if (
|
|
1437
|
+
hit &&
|
|
1438
|
+
hit.mode !== "await" &&
|
|
1439
|
+
entry.facade !== NEVER_CONSULTS &&
|
|
1440
|
+
isDeclarableAt(entry.gate, moment)
|
|
1441
|
+
) {
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
const declarable = isDeclarableAt(entry.gate, moment);
|
|
1445
|
+
findings.push({
|
|
1446
|
+
gate: entry.gate,
|
|
1447
|
+
label: REGISTRY[entry.gate]?.label ?? entry.gate,
|
|
1448
|
+
command: entry.command,
|
|
1449
|
+
surface: entry.surface,
|
|
1450
|
+
artifact: entry.artifact,
|
|
1451
|
+
job: entry.job,
|
|
1452
|
+
steps: [...entry.steps],
|
|
1453
|
+
facade: entry.facade,
|
|
1454
|
+
declarable,
|
|
1455
|
+
declaration: declarable
|
|
1456
|
+
? `"gates": { "${entry.gate}": { "${moment}": "required" } }`
|
|
1457
|
+
: null,
|
|
1458
|
+
reason: reasonFor(entry, hit, declarable, moment),
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
return findings;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Why one invocation is ungoverned, in one operator-readable sentence.
|
|
1466
|
+
* @param {object} entry The inventory entry.
|
|
1467
|
+
* @param {object|undefined} hit The resolved declaration, if any.
|
|
1468
|
+
* @param {boolean} declarable Whether a declaration at this moment is legal.
|
|
1469
|
+
* @param {string} moment The moment being proved.
|
|
1470
|
+
* @returns {string} The reason.
|
|
1471
|
+
*/
|
|
1472
|
+
function reasonFor(entry, hit, declarable, moment) {
|
|
1473
|
+
if (entry.facade === NEVER_CONSULTS) {
|
|
1474
|
+
return `${entry.artifact} contains no gate lookup at all, so no declaration can replace it.`;
|
|
1475
|
+
}
|
|
1476
|
+
if (!declarable) {
|
|
1477
|
+
return `${entry.gate} cannot legally be declared at "${moment}" (legal moments: ${(REGISTRY[entry.gate]?.moments ?? []).join(", ")}), so no configuration governs this.`;
|
|
1478
|
+
}
|
|
1479
|
+
if (hit?.mode === "await") {
|
|
1480
|
+
return `${entry.gate} is declared "await" at "${moment}", which proves nothing here, so the built-in runs.`;
|
|
1481
|
+
}
|
|
1482
|
+
return `${entry.gate} is not declared at "${moment}", so the built-in runs.`;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/**
|
|
1486
|
+
* A `gates` block that declares what the built-ins are proving today.
|
|
1487
|
+
*
|
|
1488
|
+
* SEEDING IS THE MECHANISM THAT RETIRES A FALLBACK, and it is only safe if the
|
|
1489
|
+
* seeded declaration runs the same thing the fallback did. Two rules enforce
|
|
1490
|
+
* that, and they are why this returns less than the full registry:
|
|
1491
|
+
*
|
|
1492
|
+
* * A gate is seeded only when the task that reproduces the built-in is a
|
|
1493
|
+
* script the project ACTUALLY HAS. Twenty of the registry's default tasks
|
|
1494
|
+
* are shipped by no template, so seeding by default would declare gates
|
|
1495
|
+
* whose prover does not exist — a red fleet, delivered by a version bump.
|
|
1496
|
+
* * A gate is seeded only at a moment the registry says it may be declared
|
|
1497
|
+
* at. `test-meaningfulness` runs at push and cannot be declared there;
|
|
1498
|
+
* seeding it would produce a config `validate` refuses.
|
|
1499
|
+
*
|
|
1500
|
+
* What is NOT seeded stays ungoverned, and stays reported by `unconfiguredAt`.
|
|
1501
|
+
* That is the honest outcome: this closes the gap it can prove it closes, and
|
|
1502
|
+
* leaves the rest visible rather than declaring it away.
|
|
1503
|
+
* @param {object} options Inputs.
|
|
1504
|
+
* @param {object} [options.gates] The existing gates block; its declarations win.
|
|
1505
|
+
* @param {Record<string, string>} [options.scripts] The project's package.json scripts.
|
|
1506
|
+
* @param {string} [options.runner] Task runner to record, when none is declared.
|
|
1507
|
+
* @returns {{gates: object, seeded: Array<{gate: string, moment: string, run: string|null}>, skipped: Array<{gate: string, moment: string, reason: string}>}} The merged block and what it did.
|
|
1508
|
+
*/
|
|
1509
|
+
export function seedGates({ gates = {}, scripts = {}, runner } = {}) {
|
|
1510
|
+
const seeded = [];
|
|
1511
|
+
const skipped = [];
|
|
1512
|
+
const next = { ...gates };
|
|
1513
|
+
if (runner !== undefined && next.runner === undefined) next.runner = runner;
|
|
1514
|
+
|
|
1515
|
+
for (const entry of HARDCODED_INVOCATIONS) {
|
|
1516
|
+
const { gate, moment } = entry;
|
|
1517
|
+
if (entry.facade === NEVER_CONSULTS) {
|
|
1518
|
+
skipped.push({
|
|
1519
|
+
gate,
|
|
1520
|
+
moment,
|
|
1521
|
+
reason: `${entry.artifact} consults no declaration, so a declaration would not take it over.`,
|
|
1522
|
+
});
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
if (next[gate]?.[moment] !== undefined) {
|
|
1526
|
+
skipped.push({ gate, moment, reason: "already declared." });
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
if (!isDeclarableAt(gate, moment)) {
|
|
1530
|
+
skipped.push({
|
|
1531
|
+
gate,
|
|
1532
|
+
moment,
|
|
1533
|
+
reason: `"${moment}" is not a legal moment for ${gate}.`,
|
|
1534
|
+
});
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
const run = seedTask(entry, scripts);
|
|
1538
|
+
if (run === null) {
|
|
1539
|
+
skipped.push({
|
|
1540
|
+
gate,
|
|
1541
|
+
moment,
|
|
1542
|
+
reason:
|
|
1543
|
+
entry.seedRun !== null && entry.seedRun.length === 0
|
|
1544
|
+
? "no task reproduces the built-in, so declaring it would change what runs."
|
|
1545
|
+
: `no script in package.json reproduces the built-in (looked for ${candidatesFor(entry).join(", ")}).`,
|
|
1546
|
+
});
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
// The registry default is left IMPLICIT. Writing `run` when it already
|
|
1550
|
+
// matches would freeze a default the registry is allowed to improve, and a
|
|
1551
|
+
// project that never chose the task should not be recorded as having.
|
|
1552
|
+
const useDefault =
|
|
1553
|
+
run === REGISTRY[gate]?.taskAt?.[momentFamily(moment)] ||
|
|
1554
|
+
(REGISTRY[gate]?.taskAt?.[momentFamily(moment)] === undefined &&
|
|
1555
|
+
run === REGISTRY[gate]?.task);
|
|
1556
|
+
next[gate] = {
|
|
1557
|
+
...next[gate],
|
|
1558
|
+
[moment]: useDefault ? "required" : { level: "required", run },
|
|
1559
|
+
};
|
|
1560
|
+
seeded.push({ gate, moment, run: useDefault ? null : run });
|
|
1561
|
+
}
|
|
1562
|
+
return { gates: next, seeded, skipped };
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* The task names a seeded declaration would consider for one entry.
|
|
1567
|
+
* @param {object} entry An inventory entry.
|
|
1568
|
+
* @returns {string[]} Candidate task names, in preference order.
|
|
1569
|
+
*/
|
|
1570
|
+
function candidatesFor(entry) {
|
|
1571
|
+
if (entry.seedRun !== null) return [...entry.seedRun];
|
|
1572
|
+
const definition = REGISTRY[entry.gate];
|
|
1573
|
+
const preferred =
|
|
1574
|
+
definition?.taskAt?.[momentFamily(entry.moment)] ?? definition?.task;
|
|
1575
|
+
return preferred === undefined || preferred === null ? [] : [preferred];
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* The task a seeded declaration would name, or null when none is available.
|
|
1580
|
+
* @param {object} entry An inventory entry.
|
|
1581
|
+
* @param {Record<string, string>} scripts The project's package.json scripts.
|
|
1582
|
+
* @returns {string|null} The task, or null.
|
|
1583
|
+
*/
|
|
1584
|
+
function seedTask(entry, scripts) {
|
|
1585
|
+
for (const candidate of candidatesFor(entry)) {
|
|
1586
|
+
if (Object.hasOwn(scripts, candidate)) return candidate;
|
|
1587
|
+
}
|
|
1588
|
+
return null;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
736
1591
|
/**
|
|
737
1592
|
* Every `skip_jobs` token `quality.yml` honours or advertises, and the jobs it
|
|
738
1593
|
* suppresses.
|
|
@@ -793,6 +1648,7 @@ export const SKIP_JOB_TOKENS = Object.freeze({
|
|
|
793
1648
|
environment_reset: Object.freeze(["environment_reset"]),
|
|
794
1649
|
environment_reseed: Object.freeze(["environment_reseed"]),
|
|
795
1650
|
dead_code: Object.freeze(["dead_code"]),
|
|
1651
|
+
conflict_markers: Object.freeze(["conflict_markers"]),
|
|
796
1652
|
sg_scan: Object.freeze(["sg_scan"]),
|
|
797
1653
|
floor_collisions: Object.freeze(["floor_collisions"]),
|
|
798
1654
|
npm_security_scan: Object.freeze(["npm_security_scan"]),
|
|
@@ -1218,6 +2074,30 @@ export function readGates(cwd = process.cwd()) {
|
|
|
1218
2074
|
return { runner, gates, policy: config.policy ?? {}, config };
|
|
1219
2075
|
}
|
|
1220
2076
|
|
|
2077
|
+
/**
|
|
2078
|
+
* The project's package.json scripts, for deciding what a seed may declare.
|
|
2079
|
+
*
|
|
2080
|
+
* Returns `{}` rather than throwing on an absent or unreadable manifest: the
|
|
2081
|
+
* only consumer is `seedGates`, and "no scripts" is exactly the answer that
|
|
2082
|
+
* makes it seed nothing — which is the safe direction. A malformed manifest is
|
|
2083
|
+
* a different claim and does throw, for the reason the resolve step in
|
|
2084
|
+
* `quality.yml` keeps stderr: a parse failure read as an empty result is a
|
|
2085
|
+
* broken command reported as a measured zero.
|
|
2086
|
+
* @param {string} [cwd] Directory holding package.json.
|
|
2087
|
+
* @returns {Record<string, string>} The scripts block.
|
|
2088
|
+
*/
|
|
2089
|
+
export function readScripts(cwd = process.cwd()) {
|
|
2090
|
+
const path = join(cwd, "package.json");
|
|
2091
|
+
if (!existsSync(path)) return {};
|
|
2092
|
+
let manifest;
|
|
2093
|
+
try {
|
|
2094
|
+
manifest = JSON.parse(readFileSync(path, "utf8"));
|
|
2095
|
+
} catch (err) {
|
|
2096
|
+
throw new Error(`package.json is not readable: ${err.message}`);
|
|
2097
|
+
}
|
|
2098
|
+
return manifest.scripts ?? {};
|
|
2099
|
+
}
|
|
2100
|
+
|
|
1221
2101
|
/**
|
|
1222
2102
|
* Whether a moment string is one Lisa understands.
|
|
1223
2103
|
* @param {string} moment Candidate moment.
|
|
@@ -1386,7 +2266,7 @@ function validateMoment(id, moment, value, known, interceptor, gateRun) {
|
|
|
1386
2266
|
);
|
|
1387
2267
|
}
|
|
1388
2268
|
if (entry.await) {
|
|
1389
|
-
if ([COMMIT, PUSH, SESSION_START,
|
|
2269
|
+
if ([COMMIT, PUSH, SESSION_START, ...TOOL_MOMENTS].includes(moment)) {
|
|
1390
2270
|
problems.push(
|
|
1391
2271
|
`gates."${id}"."${moment}" awaits "${entry.await}", but there is no ` +
|
|
1392
2272
|
`pull request yet for a signal to post against. An awaited check ` +
|
|
@@ -1973,6 +2853,118 @@ function main() {
|
|
|
1973
2853
|
return;
|
|
1974
2854
|
}
|
|
1975
2855
|
|
|
2856
|
+
if (command === "inventory") {
|
|
2857
|
+
// Deliberately NOT gated on reading .lisa.config.json. This answers "what
|
|
2858
|
+
// does Lisa run with a command written into the artifact", which is a
|
|
2859
|
+
// property of the shipped templates and has the same answer in a
|
|
2860
|
+
// repository that has declared nothing — the population it exists to
|
|
2861
|
+
// describe.
|
|
2862
|
+
const filtered = hardcodedAt({
|
|
2863
|
+
moment: flag("moment") ?? undefined,
|
|
2864
|
+
surface: flag("surface") ?? undefined,
|
|
2865
|
+
gate: flag("gate") ?? undefined,
|
|
2866
|
+
});
|
|
2867
|
+
if (rest.includes("--json")) {
|
|
2868
|
+
console.log(
|
|
2869
|
+
JSON.stringify(
|
|
2870
|
+
filtered.map(entry => ({
|
|
2871
|
+
...entry,
|
|
2872
|
+
declarable: isDeclarableAt(entry.gate, entry.moment),
|
|
2873
|
+
})),
|
|
2874
|
+
null,
|
|
2875
|
+
2
|
|
2876
|
+
)
|
|
2877
|
+
);
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
for (const entry of filtered) {
|
|
2881
|
+
const declarable = isDeclarableAt(entry.gate, entry.moment)
|
|
2882
|
+
? ""
|
|
2883
|
+
: " [NOT DECLARABLE AT THIS MOMENT]";
|
|
2884
|
+
console.log(
|
|
2885
|
+
`${entry.facade.padEnd(26)} ${entry.moment.padEnd(13)} ${entry.gate.padEnd(28)} ${entry.command}${declarable}`
|
|
2886
|
+
);
|
|
2887
|
+
console.log(`${" ".repeat(26)} ${entry.artifact}`);
|
|
2888
|
+
}
|
|
2889
|
+
console.log(`\n${filtered.length} hardcoded invocation(s).`);
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
if (command === "unconfigured") {
|
|
2894
|
+
const moment = flag("moment");
|
|
2895
|
+
if (!moment)
|
|
2896
|
+
throw new Error(
|
|
2897
|
+
"usage: lisa-gates.mjs unconfigured --moment=<moment> [--gate=<id>] [--surface=<surface>] [--json] [--format=github]"
|
|
2898
|
+
);
|
|
2899
|
+
const findings = unconfiguredAt({
|
|
2900
|
+
gates,
|
|
2901
|
+
moment,
|
|
2902
|
+
gate: flag("gate") ?? undefined,
|
|
2903
|
+
surface: flag("surface") ?? undefined,
|
|
2904
|
+
});
|
|
2905
|
+
if (rest.includes("--json")) {
|
|
2906
|
+
console.log(JSON.stringify(findings, null, 2));
|
|
2907
|
+
return;
|
|
2908
|
+
}
|
|
2909
|
+
// REPORT ONLY, ALWAYS EXIT 0. This is the cheap half of the fix and it is
|
|
2910
|
+
// deliberately behaviour-neutral: it ships to a fleet where essentially
|
|
2911
|
+
// every project would be reported, and a reporter that could fail a push
|
|
2912
|
+
// would be a new gate nobody declared. Making an absent declaration a hard
|
|
2913
|
+
// failure is the LAST step, after seeding guarantees one exists.
|
|
2914
|
+
for (const finding of findings) {
|
|
2915
|
+
const where = finding.job
|
|
2916
|
+
? `${finding.artifact} (job ${finding.job})`
|
|
2917
|
+
: finding.artifact;
|
|
2918
|
+
if (flag("format") === "github") {
|
|
2919
|
+
const takeover = finding.declaration
|
|
2920
|
+
? ` Take it over with ${finding.declaration}.`
|
|
2921
|
+
: "";
|
|
2922
|
+
console.log(
|
|
2923
|
+
`::warning::${finding.gate} is UNCONFIGURED at ${moment}. The built-in runs: ${finding.command}. ${finding.reason}${takeover}`
|
|
2924
|
+
);
|
|
2925
|
+
continue;
|
|
2926
|
+
}
|
|
2927
|
+
console.log(
|
|
2928
|
+
`⚠️ ${finding.gate} is UNCONFIGURED at ${moment} — nothing in .lisa.config.json governs it.`
|
|
2929
|
+
);
|
|
2930
|
+
console.log(` built-in: ${finding.command}`);
|
|
2931
|
+
console.log(` from: ${where}`);
|
|
2932
|
+
console.log(` why: ${finding.reason}`);
|
|
2933
|
+
if (finding.declaration) {
|
|
2934
|
+
console.log(` declare: ${finding.declaration}`);
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
if (command === "seed") {
|
|
2941
|
+
const scripts = readScripts();
|
|
2942
|
+
const seedRunner = flag("runner") ?? runner;
|
|
2943
|
+
const result = seedGates({ gates, scripts, runner: seedRunner });
|
|
2944
|
+
if (rest.includes("--json")) {
|
|
2945
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
for (const entry of result.seeded) {
|
|
2949
|
+
console.log(
|
|
2950
|
+
`+ ${entry.gate.padEnd(28)} ${entry.moment.padEnd(13)} ${entry.run ?? "(registry default)"}`
|
|
2951
|
+
);
|
|
2952
|
+
}
|
|
2953
|
+
for (const entry of result.skipped) {
|
|
2954
|
+
console.log(
|
|
2955
|
+
`- ${entry.gate.padEnd(28)} ${entry.moment.padEnd(13)} ${entry.reason}`
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
console.log(
|
|
2959
|
+
`\n${result.seeded.length} declaration(s) would be seeded, ${result.skipped.length} skipped.`
|
|
2960
|
+
);
|
|
2961
|
+
console.log(
|
|
2962
|
+
"Merge the block below into .lisa.config.json, then re-run `validate`:"
|
|
2963
|
+
);
|
|
2964
|
+
console.log(JSON.stringify({ gates: result.gates }, null, 2));
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
1976
2968
|
if (command === "audit-config") {
|
|
1977
2969
|
const findings = auditConfigKeys(config);
|
|
1978
2970
|
for (const finding of findings) console.error(` ${finding.message}`);
|
|
@@ -1981,7 +2973,7 @@ function main() {
|
|
|
1981
2973
|
}
|
|
1982
2974
|
|
|
1983
2975
|
throw new Error(
|
|
1984
|
-
"usage: lisa-gates.mjs validate|list|needs|contexts|skip-jobs|audit-config"
|
|
2976
|
+
"usage: lisa-gates.mjs validate|list|needs|contexts|skip-jobs|audit-config|inventory|unconfigured|seed"
|
|
1985
2977
|
);
|
|
1986
2978
|
}
|
|
1987
2979
|
|