@codyswann/lisa 4.27.4 → 4.27.6

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.
Files changed (103) hide show
  1. package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +27 -4
  2. package/all/copy-overwrite/scripts/lisa-work-item.mjs +167 -2
  3. package/dist/cli/doctor-enforcement-coverage.d.ts +14 -0
  4. package/dist/cli/doctor-enforcement-coverage.d.ts.map +1 -0
  5. package/dist/cli/doctor-enforcement-coverage.js +103 -0
  6. package/dist/cli/doctor-enforcement-coverage.js.map +1 -0
  7. package/dist/cli/doctor.d.ts.map +1 -1
  8. package/dist/cli/doctor.js +9 -0
  9. package/dist/cli/doctor.js.map +1 -1
  10. package/dist/core/enforcement-census-report.d.ts +19 -0
  11. package/dist/core/enforcement-census-report.d.ts.map +1 -0
  12. package/dist/core/enforcement-census-report.js +198 -0
  13. package/dist/core/enforcement-census-report.js.map +1 -0
  14. package/dist/core/enforcement-census.d.ts +177 -0
  15. package/dist/core/enforcement-census.d.ts.map +1 -0
  16. package/dist/core/enforcement-census.js +252 -0
  17. package/dist/core/enforcement-census.js.map +1 -0
  18. package/dist/core/enforcement-coverage.d.ts +168 -0
  19. package/dist/core/enforcement-coverage.d.ts.map +1 -0
  20. package/dist/core/enforcement-coverage.js +342 -0
  21. package/dist/core/enforcement-coverage.js.map +1 -0
  22. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  23. package/dist/core/lisa-owned-hash-ledger.js +10 -0
  24. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  25. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  26. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  27. package/dist/core/upstream-evidence-manifest.js +25 -7
  28. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  29. package/dist/migrations/ensure-push-default-safe.d.ts +50 -0
  30. package/dist/migrations/ensure-push-default-safe.d.ts.map +1 -0
  31. package/dist/migrations/ensure-push-default-safe.js +159 -0
  32. package/dist/migrations/ensure-push-default-safe.js.map +1 -0
  33. package/dist/migrations/index.d.ts +1 -0
  34. package/dist/migrations/index.d.ts.map +1 -1
  35. package/dist/migrations/index.js +3 -0
  36. package/dist/migrations/index.js.map +1 -1
  37. package/package.json +4 -3
  38. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/SKILL.md +23 -0
  41. package/plugins/lisa/.codex-plugin/skills/lisa-sync-down/SKILL.md +8 -1
  42. package/plugins/lisa/skills/lisa-setup-automations/SKILL.md +23 -0
  43. package/plugins/lisa/skills/lisa-sync-down/SKILL.md +8 -1
  44. package/plugins/lisa-agy/plugin.json +1 -1
  45. package/plugins/lisa-agy/skills/lisa-setup-automations/SKILL.md +23 -0
  46. package/plugins/lisa-agy/skills/lisa-sync-down/SKILL.md +8 -1
  47. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  50. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-copilot/skills/lisa-setup-automations/SKILL.md +23 -0
  54. package/plugins/lisa-copilot/skills/lisa-sync-down/SKILL.md +8 -1
  55. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-cursor/skills/lisa-setup-automations/SKILL.md +23 -0
  57. package/plugins/lisa-cursor/skills/lisa-sync-down/SKILL.md +8 -1
  58. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-expo-agy/plugin.json +1 -1
  61. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  66. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  70. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  71. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  75. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  76. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  80. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  81. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  85. package/plugins/lisa-rails-agy/plugin.json +1 -1
  86. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  90. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  91. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  95. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  96. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  98. package/plugins/src/base/skills/lisa-setup-automations/SKILL.md +23 -0
  99. package/plugins/src/base/skills/lisa-sync-down/SKILL.md +8 -1
  100. package/scripts/lisa-commit-and-pr-local.sh +22 -6
  101. package/scripts/lisa-enforcement-census.mjs +168 -0
  102. package/scripts/lisa-enforcement-fallback.sh +27 -4
  103. package/typescript/copy-contents/.husky/pre-push +70 -0
@@ -0,0 +1,198 @@
1
+ import { isInstallBehindDeclared, redactedLabel, } from "./enforcement-census.js";
2
+ /** Where a host tree lives inside a checkout. */
3
+ const HOST_TREE = "scripts/lisa-hooks";
4
+ /** Where the monorepo's own tree lives inside a checkout. */
5
+ const PLUGIN_TREE = "plugins/lisa/hooks";
6
+ /** What dates a host tree. */
7
+ const HOST_DATED_BY = ".lisa/apply-receipt.json";
8
+ /** What dates a plugin tree. */
9
+ const PLUGIN_DATED_BY = "plugins/lisa/.claude-plugin/plugin.json";
10
+ /** What a redacted field says instead of a real path. */
11
+ const REDACTED = "redacted";
12
+ /**
13
+ * Replace every real path in a census with a stable anonymous label.
14
+ *
15
+ * The paths this census reads name real local checkouts, and they embed client
16
+ * and product names that must never reach a public issue, PR, or commit. The
17
+ * counts and the per-checkout rows survive redaction intact, so a fleet finding
18
+ * loses nothing by being quoted safely.
19
+ * @param census - A measured fleet
20
+ * @returns The same fleet with paths replaced by derived labels
21
+ */
22
+ export function redactCensus(census) {
23
+ const scrubTree = (tree, label) => ({
24
+ ...tree,
25
+ root: `${label}/${tree.tree === "host" ? HOST_TREE : PLUGIN_TREE}`,
26
+ datedBy: `${label}/${tree.tree === "host" ? HOST_DATED_BY : PLUGIN_DATED_BY}`,
27
+ });
28
+ const scrub = (coverage) => {
29
+ const label = redactedLabel(coverage.checkoutPath);
30
+ return {
31
+ ...coverage,
32
+ label,
33
+ checkoutPath: label,
34
+ guards: coverage.guards.map(guard => ({ ...guard, script: null })),
35
+ trees: coverage.trees.map(tree => scrubTree(tree, label)),
36
+ governing: coverage.governing === null
37
+ ? null
38
+ : scrubTree(coverage.governing, label),
39
+ };
40
+ };
41
+ return {
42
+ ...census,
43
+ rosterOrigin: REDACTED,
44
+ referenceSource: REDACTED,
45
+ checkouts: census.checkouts.map(scrub),
46
+ };
47
+ }
48
+ /**
49
+ * Describe which copy governs a checkout and how old it is.
50
+ * @param coverage - One record
51
+ * @param reference - Newest Lisa the run could point at
52
+ * @returns One operator-readable clause
53
+ */
54
+ function describeGoverning(coverage, reference) {
55
+ const governing = coverage.governing;
56
+ if (governing === null)
57
+ return "no copy in force";
58
+ const where = governing.tree === "host" ? HOST_TREE : PLUGIN_TREE;
59
+ if (governing.version === null) {
60
+ return `governed by ${where} — vintage unknown, so it cannot be shown current`;
61
+ }
62
+ if (coverage.vintage === "behind") {
63
+ return `governed by ${where} — lisa ${governing.version}, behind ${reference ?? "the newest on this disk"}`;
64
+ }
65
+ return `governed by ${where} — lisa ${governing.version}`;
66
+ }
67
+ /**
68
+ * Say what a checkout has installed against what it declares.
69
+ * @param coverage - One record
70
+ * @returns One operator-readable clause
71
+ */
72
+ function describeInstallDrift(coverage) {
73
+ return `installed ${coverage.install.installed ?? "?"}, declares ${coverage.install.declared ?? "?"}`;
74
+ }
75
+ /**
76
+ * One row for a checkout that resolves at least one guard.
77
+ * @param coverage - One record
78
+ * @param reference - Newest Lisa the run could point at
79
+ * @returns The row
80
+ */
81
+ function resolvingRow(coverage, reference) {
82
+ const parts = [
83
+ describeGoverning(coverage, reference),
84
+ coverage.receipt.present ? null : "no apply receipt",
85
+ coverage.resolution === "partial"
86
+ ? `unresolved: ${coverage.unresolvedGuards.join(", ")}`
87
+ : null,
88
+ isInstallBehindDeclared(coverage) ? describeInstallDrift(coverage) : null,
89
+ ].filter((part) => part !== null);
90
+ return ` - ${coverage.label} — ${parts.join("; ")}`;
91
+ }
92
+ /**
93
+ * Build the section for checkouts that resolve nothing.
94
+ *
95
+ * First in the report, and never merged into the stale section below it. These
96
+ * are not behind — they are unenforced, and the remedy is different.
97
+ * @param census - A measured fleet
98
+ * @returns The section
99
+ */
100
+ function unguardedSection(census) {
101
+ const rows = census.checkouts
102
+ .filter(entry => entry.resolution === "none")
103
+ .map(entry => ` - ${entry.label}`);
104
+ return {
105
+ heading: `NOT ENFORCING — ${rows.length} of ${census.summary.total} resolve no guard at all`,
106
+ note: "An agent working in one of these is checked by nothing, and nothing in the session says so. Repair: run `npx @codyswann/lisa apply` in that checkout.",
107
+ rows,
108
+ };
109
+ }
110
+ /**
111
+ * Build the section for checkouts the census could not read.
112
+ * @param census - A measured fleet
113
+ * @returns The section
114
+ */
115
+ function unreadableSection(census) {
116
+ const rows = census.checkouts
117
+ .filter(entry => entry.resolution === "unreadable")
118
+ .map(entry => ` - ${entry.label} — ${entry.unreadableReason ?? "unknown"}`);
119
+ return {
120
+ heading: `COULD NOT LOOK — ${rows.length} of ${census.summary.total}`,
121
+ note: "Not counted as covered and not counted as unenforced: the census does not know. Repair: fix the path on the roster, or remove it.",
122
+ rows,
123
+ };
124
+ }
125
+ /**
126
+ * Build the section for checkouts that resolve at least one guard.
127
+ * @param census - A measured fleet
128
+ * @returns The section
129
+ */
130
+ function resolvingSection(census) {
131
+ const { summary } = census;
132
+ const rows = census.checkouts
133
+ .filter(entry => entry.resolution === "partial" || entry.resolution === "full")
134
+ .map(entry => resolvingRow(entry, census.reference));
135
+ return {
136
+ heading: `ENFORCING — ${summary.resolving} of ${summary.total} (${summary.partial} of them only partly)`,
137
+ note: `behind: ${summary.behind} · vintage unknown: ${summary.undateable} · current: ${summary.current} · no apply receipt: ${summary.withoutReceipt}`,
138
+ rows,
139
+ };
140
+ }
141
+ /**
142
+ * Build the section for checkouts whose installed Lisa is behind their own
143
+ * manifest.
144
+ *
145
+ * Its own section because it fails in the affirmative: these checkouts resolve
146
+ * guards, run them, and answer confidently about a version nobody is using.
147
+ * @param census - A measured fleet
148
+ * @returns The section
149
+ */
150
+ function installDriftSection(census) {
151
+ const rows = census.checkouts
152
+ .filter(entry => isInstallBehindDeclared(entry))
153
+ .map(entry => ` - ${entry.label} — ${describeInstallDrift(entry)}`);
154
+ return {
155
+ heading: `INSTALLED BEHIND DECLARED — ${rows.length} of ${census.summary.total}`,
156
+ note: "These answer every question confidently about a Lisa version nobody is running. Repair: reinstall dependencies in that checkout.",
157
+ rows,
158
+ };
159
+ }
160
+ /**
161
+ * Render one section, or nothing when it is empty and uninteresting.
162
+ * @param section - The section
163
+ * @param always - Whether to print it even when empty
164
+ * @returns The rendered lines
165
+ */
166
+ function renderSection(section, always) {
167
+ if (section.rows.length === 0 && !always)
168
+ return [];
169
+ return [
170
+ "",
171
+ section.heading,
172
+ ` ${section.note}`,
173
+ ...(section.rows.length === 0 ? [" (none)"] : section.rows),
174
+ ];
175
+ }
176
+ /**
177
+ * Render the whole census for a terminal.
178
+ * @param census - A measured fleet
179
+ * @returns The report
180
+ */
181
+ export function renderFleetCensusReport(census) {
182
+ const { summary } = census;
183
+ const lines = [
184
+ `Lisa enforcement fleet census — ${census.measuredAt}`,
185
+ `Roster: ${census.rosterOrigin} (${summary.total} checkout${summary.total === 1 ? "" : "s"})`,
186
+ `Newest Lisa found: ${census.reference ?? "none"} (${census.referenceSource})`,
187
+ ...renderSection(unguardedSection(census), true),
188
+ ...renderSection(unreadableSection(census), true),
189
+ ...renderSection(resolvingSection(census), true),
190
+ ...renderSection(installDriftSection(census), false),
191
+ "",
192
+ `Covered — all six guards, a current copy, and an apply receipt: ${summary.covered} of ${summary.total}.`,
193
+ "This census reports and never gates: its exit status is 0 whatever it finds.",
194
+ "A checkout not on the roster was not measured, which is not the same as covered.",
195
+ ];
196
+ return `${lines.join("\n")}\n`;
197
+ }
198
+ //# sourceMappingURL=enforcement-census-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enforcement-census-report.js","sourceRoot":"","sources":["../../src/core/enforcement-census-report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,177 @@
1
+ import type { CheckoutCoverage } from "./enforcement-coverage.js";
2
+ /** Where a checkout on the roster came from. */
3
+ export type RosterSource = "roster" | "scan" | "argument";
4
+ /** One checkout the census intends to look at. */
5
+ export interface RosterEntry {
6
+ /** Operator-facing label. */
7
+ readonly label: string;
8
+ /** Absolute path to look at. */
9
+ readonly checkoutPath: string;
10
+ /** How this entry reached the roster. */
11
+ readonly source: RosterSource;
12
+ }
13
+ /**
14
+ * The disjoint classes a fleet falls into.
15
+ *
16
+ * `unreadable`, `unguarded`, `partial` and `full` partition the roster — they
17
+ * sum to `total` by construction. `behind`, `undateable` and `current`
18
+ * partition the *resolving* subset only, and never see an unguarded or
19
+ * unreadable checkout.
20
+ */
21
+ export interface FleetCensusSummary {
22
+ /** Checkouts the census set out to look at. */
23
+ readonly total: number;
24
+ /** Checkouts it could not look into. Never counted as covered. */
25
+ readonly unreadable: number;
26
+ /** Checkouts that resolve NO guard. Not stale — unenforced. */
27
+ readonly unguarded: number;
28
+ /** Checkouts that resolve some guards but not all six. */
29
+ readonly partial: number;
30
+ /** Checkouts that resolve all six guards. */
31
+ readonly full: number;
32
+ /** Checkouts resolving at least one guard. */
33
+ readonly resolving: number;
34
+ /** Resolving checkouts whose governing copy is behind the reference. */
35
+ readonly behind: number;
36
+ /** Resolving checkouts whose governing copy cannot be dated at all. */
37
+ readonly undateable: number;
38
+ /** Resolving checkouts whose governing copy is current. */
39
+ readonly current: number;
40
+ /** Resolving checkouts carrying no apply receipt. */
41
+ readonly withoutReceipt: number;
42
+ /** Checkouts whose installed Lisa is behind the version they declare. */
43
+ readonly installBehindDeclared: number;
44
+ /** All six guards, current copy, apply receipt present. */
45
+ readonly covered: number;
46
+ }
47
+ /** One complete census run. */
48
+ export interface FleetCensus {
49
+ /** When the census ran. */
50
+ readonly measuredAt: string;
51
+ /** Newest Lisa the run could point at, and what proved it. */
52
+ readonly reference: string | null;
53
+ /** Where the reference came from, in the operator's language. */
54
+ readonly referenceSource: string;
55
+ /** Where the roster came from. */
56
+ readonly rosterOrigin: string;
57
+ /** One record per checkout, in roster order. */
58
+ readonly checkouts: readonly CheckoutCoverage[];
59
+ /** The disjoint classes. */
60
+ readonly summary: FleetCensusSummary;
61
+ }
62
+ /** The fleet roster `scripts/lisa-update-local.sh` already maintains. */
63
+ export declare const DEFAULT_ROSTER_FILE = ".lisa.workspaces.json";
64
+ /**
65
+ * Read a fleet roster file.
66
+ *
67
+ * The primary shape is the one already on disk: an object mapping checkout path
68
+ * to target branch, which `scripts/lisa-update-local.sh` reads. An array of
69
+ * paths is accepted too, so a census can be pointed at an ad-hoc list without
70
+ * inventing a branch for each entry.
71
+ * @param rosterPath - Path to the roster file
72
+ * @returns The checkouts it names, or null when it cannot be read
73
+ */
74
+ export declare function readFleetRoster(rosterPath: string): Promise<readonly RosterEntry[] | null>;
75
+ /**
76
+ * Find checkouts under a directory, without a roster.
77
+ *
78
+ * Discovery exists because a hand-maintained roster can go stale the same way
79
+ * the frozen comment did: a checkout nobody added is a checkout the census
80
+ * cannot see, and an unseen checkout reads as coverage it does not have. A
81
+ * candidate is any directory containing a `.git` entry, found no deeper than
82
+ * `depth` levels so the walk stays bounded on a large disk.
83
+ * @param root - Directory to search
84
+ * @param depth - How many levels below `root` to descend
85
+ * @returns The checkouts found, in listing order
86
+ */
87
+ export declare function scanForCheckouts(root: string, depth: number): Promise<readonly RosterEntry[]>;
88
+ /**
89
+ * Collapse entries that name the same checkout twice.
90
+ *
91
+ * Two roster spellings of one directory, or a roster entry a scan also found,
92
+ * would otherwise be counted twice — inflating both the numerator and the
93
+ * denominator and making the census disagree with itself.
94
+ * @param entries - Entries from every source
95
+ * @returns One entry per distinct checkout, first spelling wins
96
+ */
97
+ export declare function dedupeRoster(entries: readonly RosterEntry[]): Promise<readonly RosterEntry[]>;
98
+ /**
99
+ * Count the resolving checkouts whose governing copy is behind.
100
+ *
101
+ * Defined over the resolving set on purpose, and named so that folding the
102
+ * unguarded class into it is a visible edit rather than an accident. A checkout
103
+ * that resolves nothing has no copy to be behind: calling it stale describes a
104
+ * guard that would refuse things, and there is none.
105
+ * @param checkouts - Every record in the run
106
+ * @returns How many resolving checkouts are behind the reference
107
+ */
108
+ export declare function countStale(checkouts: readonly CheckoutCoverage[]): number;
109
+ /**
110
+ * Reduce a run to its disjoint classes.
111
+ * @param checkouts - Every record in the run
112
+ * @returns The summary
113
+ */
114
+ export declare function summarizeFleetCensus(checkouts: readonly CheckoutCoverage[]): FleetCensusSummary;
115
+ /**
116
+ * Whether a checkout's installed Lisa is older than the one it declares.
117
+ *
118
+ * The comparison is against the range's own floor, read as a plain version once
119
+ * the usual range punctuation is stripped. A range this cannot parse yields
120
+ * `false` — absence of proof never becomes proof of drift.
121
+ * @param coverage - One record
122
+ * @returns Whether the installed copy is behind the declared one
123
+ */
124
+ export declare function isInstallBehindDeclared(coverage: CheckoutCoverage): boolean;
125
+ /**
126
+ * Resolve the newest Lisa this run can point at.
127
+ *
128
+ * A maximum over evidence found on the same disk during the same run, never a
129
+ * network lookup and never a nominated authority. A copy is reported behind
130
+ * only when something newer can actually be pointed at, so the worst case is
131
+ * silence rather than a fleet-wide false alarm.
132
+ * @param checkouts - Records collected with no reference
133
+ * @param seed - A version the caller can vouch for, such as its own manifest
134
+ * @returns The reference version and what proved it
135
+ */
136
+ export declare function resolveReference(checkouts: readonly CheckoutCoverage[], seed: string | null): {
137
+ readonly version: string | null;
138
+ readonly source: string;
139
+ };
140
+ /** Everything a census run needs. */
141
+ export interface FleetCensusOptions {
142
+ /** Checkouts to look at, already deduped. */
143
+ readonly roster: readonly RosterEntry[];
144
+ /** Where that roster came from, for the report header. */
145
+ readonly rosterOrigin: string;
146
+ /** A version the caller can vouch for, folded into the reference. */
147
+ readonly seedVersion?: string | null;
148
+ /** An explicit reference, which suppresses the maximum entirely. */
149
+ readonly reference?: string | null;
150
+ /** Clock seam, so the report is reproducible under test. */
151
+ readonly now?: () => Date;
152
+ }
153
+ /**
154
+ * Run the census.
155
+ *
156
+ * Never throws for a fleet finding and never returns a status: the caller
157
+ * prints this and exits 0. The only thing that can fail here is the census
158
+ * itself, and a checkout it cannot read comes back as `unreadable` rather than
159
+ * as an exception, because one unreachable directory must not stop the other
160
+ * twenty-six from being measured.
161
+ * @param options - Roster and reference inputs
162
+ * @returns The measured fleet
163
+ */
164
+ export declare function runFleetCensus(options: FleetCensusOptions): Promise<FleetCensus>;
165
+ /**
166
+ * A stable, path-free label for a checkout.
167
+ *
168
+ * The census names real local checkouts, and those paths embed client and
169
+ * product names that must never reach a public issue or PR. Redaction keeps the
170
+ * counts and the per-checkout rows — which is all a fleet finding needs — while
171
+ * making the report safe to paste. It is derived from the path, so the same
172
+ * checkout carries the same label between runs and can still be discussed.
173
+ * @param checkoutPath - The real path
174
+ * @returns A stable anonymous label
175
+ */
176
+ export declare function redactedLabel(checkoutPath: string): string;
177
+ //# sourceMappingURL=enforcement-census.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enforcement-census.d.ts","sourceRoot":"","sources":["../../src/core/enforcement-census.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,252 @@
1
+ /**
2
+ * The fleet census: how many checkouts resolve enforcement, re-derived on
3
+ * every run.
4
+ *
5
+ * The per-session staleness notice reports **this** checkout to **this**
6
+ * session, and it is right to do so — it keeps running while stale, and it
7
+ * prints once per session rather than per call, both deliberately. The gap it
8
+ * leaves is that nobody was looking at the fleet, so the only fleet-wide number
9
+ * that ever existed was hand-taken and frozen in a code comment
10
+ * (CodySwannGT/lisa#3490).
11
+ *
12
+ * Two rules shape everything below, and both are learned from that notice:
13
+ *
14
+ * - **Report, do not gate.** No finding about the fleet may change the exit
15
+ * status. Reddening a build because someone else's checkout is stale would
16
+ * be a worse control than the one it replaces, and it would be routed
17
+ * around — and a guard routed around protects nothing.
18
+ * - **"Resolves nothing" and "resolves something old" are different
19
+ * findings.** They have different remedies, and folding them into one "N
20
+ * stale" number loses the serious half — which is exactly how the
21
+ * unenforced checkouts stayed easy to miss. They are separate
22
+ * fields here, and `countStale` is defined over the resolving set alone, so
23
+ * folding them takes a code change that a named test refuses.
24
+ *
25
+ * Nothing in this module stores a count. The roster names where to look, the
26
+ * checkouts on disk answer, and the reference version is a maximum over the
27
+ * evidence found during the same run. A census that restated today's number in
28
+ * a different file would go stale exactly the way the comment did.
29
+ * @module core/enforcement-census
30
+ */
31
+ import { createHash } from "node:crypto";
32
+ import { readdir, realpath } from "node:fs/promises";
33
+ import * as os from "node:os";
34
+ import * as path from "node:path";
35
+ import { applyReference, collectCheckoutCoverage, isOlderVersion, versionEvidence, } from "./enforcement-coverage.js";
36
+ import { readJsonOrNull } from "../utils/json-utils.js";
37
+ import { pathExists } from "../utils/file-operations.js";
38
+ /** The fleet roster `scripts/lisa-update-local.sh` already maintains. */
39
+ export const DEFAULT_ROSTER_FILE = ".lisa.workspaces.json";
40
+ /**
41
+ * Expand a leading `~` the way the roster's other reader does.
42
+ * @param value - A path that may start with `~`
43
+ * @returns The expanded path
44
+ */
45
+ function expandHome(value) {
46
+ return value.startsWith("~")
47
+ ? path.join(os.homedir(), value.slice(1))
48
+ : value;
49
+ }
50
+ /**
51
+ * Read a fleet roster file.
52
+ *
53
+ * The primary shape is the one already on disk: an object mapping checkout path
54
+ * to target branch, which `scripts/lisa-update-local.sh` reads. An array of
55
+ * paths is accepted too, so a census can be pointed at an ad-hoc list without
56
+ * inventing a branch for each entry.
57
+ * @param rosterPath - Path to the roster file
58
+ * @returns The checkouts it names, or null when it cannot be read
59
+ */
60
+ export async function readFleetRoster(rosterPath) {
61
+ const parsed = await readJsonOrNull(rosterPath);
62
+ if (parsed === null || typeof parsed !== "object")
63
+ return null;
64
+ const raw = Array.isArray(parsed)
65
+ ? parsed.filter((entry) => typeof entry === "string")
66
+ : Object.keys(parsed);
67
+ return raw.map(entry => ({
68
+ label: entry,
69
+ checkoutPath: path.resolve(expandHome(entry)),
70
+ source: "roster",
71
+ }));
72
+ }
73
+ /**
74
+ * Find checkouts under a directory, without a roster.
75
+ *
76
+ * Discovery exists because a hand-maintained roster can go stale the same way
77
+ * the frozen comment did: a checkout nobody added is a checkout the census
78
+ * cannot see, and an unseen checkout reads as coverage it does not have. A
79
+ * candidate is any directory containing a `.git` entry, found no deeper than
80
+ * `depth` levels so the walk stays bounded on a large disk.
81
+ * @param root - Directory to search
82
+ * @param depth - How many levels below `root` to descend
83
+ * @returns The checkouts found, in listing order
84
+ */
85
+ export async function scanForCheckouts(root, depth) {
86
+ const resolved = path.resolve(expandHome(root));
87
+ if (depth < 0 || !(await pathExists(resolved)))
88
+ return [];
89
+ if (await pathExists(path.join(resolved, ".git"))) {
90
+ return [{ label: resolved, checkoutPath: resolved, source: "scan" }];
91
+ }
92
+ const entries = await readdir(resolved, { withFileTypes: true }).catch(() => []);
93
+ const children = entries
94
+ .filter(entry => entry.isDirectory() && !entry.name.startsWith("."))
95
+ .filter(entry => entry.name !== "node_modules")
96
+ .map(entry => path.join(resolved, entry.name));
97
+ const found = await Promise.all(children.map(child => scanForCheckouts(child, depth - 1)));
98
+ return found.flat();
99
+ }
100
+ /**
101
+ * Collapse entries that name the same checkout twice.
102
+ *
103
+ * Two roster spellings of one directory, or a roster entry a scan also found,
104
+ * would otherwise be counted twice — inflating both the numerator and the
105
+ * denominator and making the census disagree with itself.
106
+ * @param entries - Entries from every source
107
+ * @returns One entry per distinct checkout, first spelling wins
108
+ */
109
+ export async function dedupeRoster(entries) {
110
+ const keyed = await Promise.all(entries.map(async (entry) => ({
111
+ entry,
112
+ key: await realpath(entry.checkoutPath).catch(() => entry.checkoutPath),
113
+ })));
114
+ return keyed
115
+ .filter((candidate, index) => keyed.findIndex(other => other.key === candidate.key) === index)
116
+ .map(candidate => candidate.entry);
117
+ }
118
+ /**
119
+ * Count the resolving checkouts whose governing copy is behind.
120
+ *
121
+ * Defined over the resolving set on purpose, and named so that folding the
122
+ * unguarded class into it is a visible edit rather than an accident. A checkout
123
+ * that resolves nothing has no copy to be behind: calling it stale describes a
124
+ * guard that would refuse things, and there is none.
125
+ * @param checkouts - Every record in the run
126
+ * @returns How many resolving checkouts are behind the reference
127
+ */
128
+ export function countStale(checkouts) {
129
+ return checkouts.filter(entry => (entry.resolution === "partial" || entry.resolution === "full") &&
130
+ entry.vintage === "behind").length;
131
+ }
132
+ /**
133
+ * Reduce a run to its disjoint classes.
134
+ * @param checkouts - Every record in the run
135
+ * @returns The summary
136
+ */
137
+ export function summarizeFleetCensus(checkouts) {
138
+ const resolving = checkouts.filter(entry => entry.resolution === "partial" || entry.resolution === "full");
139
+ const count = (list, predicate) => list.filter(predicate).length;
140
+ return {
141
+ total: checkouts.length,
142
+ unreadable: count(checkouts, entry => entry.resolution === "unreadable"),
143
+ unguarded: count(checkouts, entry => entry.resolution === "none"),
144
+ partial: count(checkouts, entry => entry.resolution === "partial"),
145
+ full: count(checkouts, entry => entry.resolution === "full"),
146
+ resolving: resolving.length,
147
+ behind: countStale(checkouts),
148
+ undateable: count(resolving, entry => entry.vintage === "undateable"),
149
+ current: count(resolving, entry => entry.vintage === "current"),
150
+ withoutReceipt: count(resolving, entry => !entry.receipt.present),
151
+ installBehindDeclared: count(checkouts, entry => isInstallBehindDeclared(entry)),
152
+ covered: count(checkouts, entry => entry.resolution === "full" &&
153
+ entry.vintage === "current" &&
154
+ entry.receipt.present),
155
+ };
156
+ }
157
+ /**
158
+ * Whether a checkout's installed Lisa is older than the one it declares.
159
+ *
160
+ * The comparison is against the range's own floor, read as a plain version once
161
+ * the usual range punctuation is stripped. A range this cannot parse yields
162
+ * `false` — absence of proof never becomes proof of drift.
163
+ * @param coverage - One record
164
+ * @returns Whether the installed copy is behind the declared one
165
+ */
166
+ export function isInstallBehindDeclared(coverage) {
167
+ const { declared, installed } = coverage.install;
168
+ if (declared === null || installed === null)
169
+ return false;
170
+ // Bounded quantifiers rather than `\d+`: a range string arrives from a
171
+ // manifest this repository does not own, and an unbounded pattern over
172
+ // untrusted input is the shape that backtracks pathologically.
173
+ const floor = /(\d{1,9}\.\d{1,9}\.\d{1,9})/u.exec(declared)?.[1];
174
+ return floor !== undefined && isOlderVersion(installed, floor);
175
+ }
176
+ /**
177
+ * Resolve the newest Lisa this run can point at.
178
+ *
179
+ * A maximum over evidence found on the same disk during the same run, never a
180
+ * network lookup and never a nominated authority. A copy is reported behind
181
+ * only when something newer can actually be pointed at, so the worst case is
182
+ * silence rather than a fleet-wide false alarm.
183
+ * @param checkouts - Records collected with no reference
184
+ * @param seed - A version the caller can vouch for, such as its own manifest
185
+ * @returns The reference version and what proved it
186
+ */
187
+ export function resolveReference(checkouts, seed) {
188
+ const candidates = checkouts.flatMap(entry => versionEvidence(entry).map(version => ({
189
+ version,
190
+ source: entry.label,
191
+ })));
192
+ const all = seed === null
193
+ ? candidates
194
+ : [
195
+ { version: seed, source: "this checkout's own manifest" },
196
+ ...candidates,
197
+ ];
198
+ const best = all.reduce((winner, candidate) => winner === null || isOlderVersion(winner.version, candidate.version)
199
+ ? candidate
200
+ : winner, null);
201
+ return best === null
202
+ ? { version: null, source: "no Lisa version could be found on this disk" }
203
+ : best;
204
+ }
205
+ /**
206
+ * Run the census.
207
+ *
208
+ * Never throws for a fleet finding and never returns a status: the caller
209
+ * prints this and exits 0. The only thing that can fail here is the census
210
+ * itself, and a checkout it cannot read comes back as `unreadable` rather than
211
+ * as an exception, because one unreachable directory must not stop the other
212
+ * twenty-six from being measured.
213
+ * @param options - Roster and reference inputs
214
+ * @returns The measured fleet
215
+ */
216
+ export async function runFleetCensus(options) {
217
+ const undated = await Promise.all(options.roster.map(entry => collectCheckoutCoverage({
218
+ label: entry.label,
219
+ checkoutPath: entry.checkoutPath,
220
+ reference: null,
221
+ })));
222
+ const resolved = options.reference != null && options.reference !== ""
223
+ ? { version: options.reference, source: "supplied by the caller" }
224
+ : resolveReference(undated, options.seedVersion ?? null);
225
+ const checkouts = undated.map(entry => applyReference(entry, resolved.version));
226
+ return {
227
+ measuredAt: (options.now?.() ?? new Date()).toISOString(),
228
+ reference: resolved.version,
229
+ referenceSource: resolved.source,
230
+ rosterOrigin: options.rosterOrigin,
231
+ checkouts,
232
+ summary: summarizeFleetCensus(checkouts),
233
+ };
234
+ }
235
+ /**
236
+ * A stable, path-free label for a checkout.
237
+ *
238
+ * The census names real local checkouts, and those paths embed client and
239
+ * product names that must never reach a public issue or PR. Redaction keeps the
240
+ * counts and the per-checkout rows — which is all a fleet finding needs — while
241
+ * making the report safe to paste. It is derived from the path, so the same
242
+ * checkout carries the same label between runs and can still be discussed.
243
+ * @param checkoutPath - The real path
244
+ * @returns A stable anonymous label
245
+ */
246
+ export function redactedLabel(checkoutPath) {
247
+ return `checkout-${createHash("sha256")
248
+ .update(checkoutPath)
249
+ .digest("hex")
250
+ .slice(0, 8)}`;
251
+ }
252
+ //# sourceMappingURL=enforcement-census.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enforcement-census.js","sourceRoot":"","sources":["../../src/core/enforcement-census.ts"],"names":[],"mappings":"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"}