@codyswann/lisa 2.262.0 → 2.263.0

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 (95) hide show
  1. package/all/copy-contents/gitignore +2 -0
  2. package/dist/cli/ui-config-write.d.ts.map +1 -1
  3. package/dist/cli/ui-config-write.js +24 -1
  4. package/dist/cli/ui-config-write.js.map +1 -1
  5. package/dist/core/learnings-lock.d.ts +7 -0
  6. package/dist/core/learnings-lock.d.ts.map +1 -1
  7. package/dist/core/learnings-lock.js +49 -8
  8. package/dist/core/learnings-lock.js.map +1 -1
  9. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  10. package/dist/core/upstream-evidence-manifest.js +17 -3
  11. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  12. package/dist/health/contract.d.ts +56 -0
  13. package/dist/health/contract.d.ts.map +1 -0
  14. package/dist/health/contract.js +153 -0
  15. package/dist/health/contract.js.map +1 -0
  16. package/dist/health/directory-sync.d.ts +17 -0
  17. package/dist/health/directory-sync.d.ts.map +1 -0
  18. package/dist/health/directory-sync.js +69 -0
  19. package/dist/health/directory-sync.js.map +1 -0
  20. package/dist/health/index.d.ts +3 -0
  21. package/dist/health/index.d.ts.map +1 -0
  22. package/dist/health/index.js +3 -0
  23. package/dist/health/index.js.map +1 -0
  24. package/dist/health/storage.d.ts +39 -0
  25. package/dist/health/storage.d.ts.map +1 -0
  26. package/dist/health/storage.js +262 -0
  27. package/dist/health/storage.js.map +1 -0
  28. package/dist/health/strict-validation.d.ts +56 -0
  29. package/dist/health/strict-validation.d.ts.map +1 -0
  30. package/dist/health/strict-validation.js +144 -0
  31. package/dist/health/strict-validation.js.map +1 -0
  32. package/dist/sync/config-sync.d.ts.map +1 -1
  33. package/dist/sync/config-sync.js +18 -6
  34. package/dist/sync/config-sync.js.map +1 -1
  35. package/dist/sync/registry.d.ts +2 -0
  36. package/dist/sync/registry.d.ts.map +1 -1
  37. package/dist/sync/registry.js +7 -0
  38. package/dist/sync/registry.js.map +1 -1
  39. package/package.json +4 -2
  40. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa/rules/reference/config-resolution.md +1 -0
  43. package/plugins/lisa-agy/plugin.json +1 -1
  44. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  47. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-copilot/rules/reference/config-resolution.md +1 -0
  51. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +1 -0
  53. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-expo-agy/plugin.json +1 -1
  56. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  61. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  66. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  70. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  71. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  75. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  76. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  80. package/plugins/lisa-rails-agy/plugin.json +1 -1
  81. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  85. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  86. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  90. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  91. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  93. package/plugins/src/base/rules/reference/config-resolution.md +1 -0
  94. package/scripts/verify-health-contract-built.mjs +143 -0
  95. package/ui/README.md +8 -3
@@ -0,0 +1,56 @@
1
+ export declare const HEALTH_SCHEDULE_VALUES: readonly ["off", "daily", "weekly"];
2
+ /** Supported unattended health-run cadence. */
3
+ export type HealthSchedule = (typeof HEALTH_SCHEDULE_VALUES)[number];
4
+ /** Origin of one health finding. */
5
+ export type HealthLayer = "deterministic" | "agentic";
6
+ /** Outcome of one health check. */
7
+ export type HealthStatus = "pass" | "warn" | "fail";
8
+ /** Scope executed by a health run. */
9
+ export type HealthMode = "deterministic" | "full";
10
+ /** Canonical aggregate health verdict. */
11
+ export type HealthVerdict = "in band" | "drift detected";
12
+ /** One bounded, attributable health-check result. */
13
+ export interface HealthFinding {
14
+ readonly check: string;
15
+ readonly layer: HealthLayer;
16
+ readonly status: HealthStatus;
17
+ readonly reason: string;
18
+ }
19
+ /** Derived zero-filled counts and verdict. */
20
+ export interface HealthSummary {
21
+ readonly verdict: HealthVerdict;
22
+ readonly counts: Readonly<{
23
+ pass: number;
24
+ warn: number;
25
+ fail: number;
26
+ }>;
27
+ }
28
+ /** Complete immutable Health v1 result envelope. */
29
+ export interface HealthResult {
30
+ readonly schemaVersion: 1;
31
+ readonly runId: string;
32
+ readonly mode: HealthMode;
33
+ readonly startedAt: string;
34
+ readonly completedAt: string;
35
+ readonly findings: readonly HealthFinding[];
36
+ readonly summary: HealthSummary;
37
+ }
38
+ /**
39
+ * Validate a health schedule value.
40
+ * @param value - Untrusted schedule candidate
41
+ * @returns Validated closed schedule value
42
+ */
43
+ export declare function validateHealthSchedule(value: unknown): HealthSchedule;
44
+ /**
45
+ * Derive zero-filled counts and the canonical verdict.
46
+ * @param findings - Untrusted finding collection
47
+ * @returns Frozen canonical summary
48
+ */
49
+ export declare function summarizeHealthFindings(findings: unknown): HealthSummary;
50
+ /**
51
+ * Validate untrusted data into a detached, deeply frozen Health v1 result.
52
+ * @param candidate - Untrusted result envelope
53
+ * @returns Frozen validated Health v1 result
54
+ */
55
+ export declare function validateHealthResult(candidate: unknown): HealthResult;
56
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/health/contract.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,sBAAsB,qCAAsC,CAAC;AAC1E,+CAA+C;AAC/C,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AACrE,oCAAoC;AACpC,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG,SAAS,CAAC;AACtD,mCAAmC;AACnC,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AACpD,sCAAsC;AACtC,MAAM,MAAM,UAAU,GAAG,eAAe,GAAG,MAAM,CAAC;AAClD,0CAA0C;AAC1C,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,gBAAgB,CAAC;AAEzD,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,8CAA8C;AAC9C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACzE;AAED,oDAAoD;AACpD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;CACjC;AAkBD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAKrE;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,OAAO,GAAG,aAAa,CAaxE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,OAAO,GAAG,YAAY,CA6CrE"}
@@ -0,0 +1,153 @@
1
+ /** Strict executable contract for Health v1 results and scheduling. */
2
+ import { hasUnsafeTextCharacter, readStrictDenseArray, readStrictProperty as read, requireBoundedMachineId, requireCanonicalUtcTimestamp, requireClosedString as requireEnum, requireStrictRecord as requireRecord, } from "./strict-validation.js";
3
+ export const HEALTH_SCHEDULE_VALUES = ["off", "daily", "weekly"];
4
+ const RESULT_FIELDS = [
5
+ "schemaVersion",
6
+ "runId",
7
+ "mode",
8
+ "startedAt",
9
+ "completedAt",
10
+ "findings",
11
+ "summary",
12
+ ];
13
+ const FINDING_FIELDS = ["check", "layer", "status", "reason"];
14
+ const SUMMARY_FIELDS = ["verdict", "counts"];
15
+ const COUNT_FIELDS = ["pass", "warn", "fail"];
16
+ const MAX_FINDINGS = 200;
17
+ const MAX_ID_BYTES = 128;
18
+ const MAX_REASON_BYTES = 2_000;
19
+ /**
20
+ * Validate a health schedule value.
21
+ * @param value - Untrusted schedule candidate
22
+ * @returns Validated closed schedule value
23
+ */
24
+ export function validateHealthSchedule(value) {
25
+ if (!HEALTH_SCHEDULE_VALUES.includes(value)) {
26
+ throw new Error("Invalid health.schedule: expected off, daily, or weekly");
27
+ }
28
+ return value;
29
+ }
30
+ /**
31
+ * Derive zero-filled counts and the canonical verdict.
32
+ * @param findings - Untrusted finding collection
33
+ * @returns Frozen canonical summary
34
+ */
35
+ export function summarizeHealthFindings(findings) {
36
+ const validated = requireFindings(findings);
37
+ const counts = validated.reduce((current, finding) => ({
38
+ ...current,
39
+ [finding.status]: current[finding.status] + 1,
40
+ }), { pass: 0, warn: 0, fail: 0 });
41
+ return Object.freeze({
42
+ verdict: counts.fail > 0 ? "drift detected" : "in band",
43
+ counts: Object.freeze(counts),
44
+ });
45
+ }
46
+ /**
47
+ * Validate untrusted data into a detached, deeply frozen Health v1 result.
48
+ * @param candidate - Untrusted result envelope
49
+ * @returns Frozen validated Health v1 result
50
+ */
51
+ export function validateHealthResult(candidate) {
52
+ const input = requireRecord(candidate, RESULT_FIELDS, "HealthResult");
53
+ if (read(input, "schemaVersion") !== 1) {
54
+ throw new Error("Invalid schemaVersion: expected 1");
55
+ }
56
+ const runId = requireBoundedMachineId(read(input, "runId"), "runId", MAX_ID_BYTES);
57
+ const mode = requireEnum(read(input, "mode"), ["deterministic", "full"], "mode");
58
+ const startedAt = requireCanonicalUtcTimestamp(read(input, "startedAt"), "startedAt");
59
+ const completedAt = requireCanonicalUtcTimestamp(read(input, "completedAt"), "completedAt");
60
+ if (completedAt < startedAt) {
61
+ throw new Error("Invalid completedAt: precedes startedAt");
62
+ }
63
+ const findings = requireFindings(read(input, "findings"));
64
+ const supplied = requireSummary(read(input, "summary"));
65
+ const derived = summarizeHealthFindings(findings);
66
+ assertModeLayers(mode, findings);
67
+ if (supplied.verdict !== derived.verdict ||
68
+ COUNT_FIELDS.some(key => supplied.counts[key] !== derived.counts[key])) {
69
+ throw new Error("Invalid summary: verdict or counts do not match findings");
70
+ }
71
+ return Object.freeze({
72
+ schemaVersion: 1,
73
+ runId,
74
+ mode,
75
+ startedAt,
76
+ completedAt,
77
+ findings: Object.freeze(findings),
78
+ summary: derived,
79
+ });
80
+ }
81
+ /**
82
+ * Validate and detach a non-empty finding list.
83
+ * @param value - Untrusted finding collection
84
+ * @returns Detached validated findings
85
+ */
86
+ function requireFindings(value) {
87
+ const findings = readStrictDenseArray(value, 1, MAX_FINDINGS, "findings").map((candidate, index) => requireFinding(candidate, index));
88
+ const checks = findings.map(finding => finding.check);
89
+ if (new Set(checks).size !== checks.length) {
90
+ throw new Error("Invalid findings: check identifiers must be unique");
91
+ }
92
+ return findings;
93
+ }
94
+ /**
95
+ * Validate one finding.
96
+ * @param candidate - Untrusted finding candidate
97
+ * @param index - Finding index used in validation messages
98
+ * @returns Frozen validated finding
99
+ */
100
+ function requireFinding(candidate, index) {
101
+ const input = requireRecord(candidate, FINDING_FIELDS, `findings[${index}]`);
102
+ const reason = read(input, "reason");
103
+ if (typeof reason !== "string" ||
104
+ reason.trim() !== reason ||
105
+ reason.length === 0 ||
106
+ Buffer.byteLength(reason, "utf8") > MAX_REASON_BYTES ||
107
+ hasUnsafeTextCharacter(reason)) {
108
+ throw new Error(`Invalid findings[${index}].reason: expected readable text`);
109
+ }
110
+ return Object.freeze({
111
+ check: requireBoundedMachineId(read(input, "check"), `findings[${index}].check`, MAX_ID_BYTES),
112
+ layer: requireEnum(read(input, "layer"), ["deterministic", "agentic"], `findings[${index}].layer`),
113
+ status: requireEnum(read(input, "status"), ["pass", "warn", "fail"], `findings[${index}].status`),
114
+ reason,
115
+ });
116
+ }
117
+ /**
118
+ * Validate supplied summary shape and scalar values before comparison.
119
+ * @param value - Untrusted summary candidate
120
+ * @returns Frozen validated supplied summary
121
+ */
122
+ function requireSummary(value) {
123
+ const summary = requireRecord(value, SUMMARY_FIELDS, "summary");
124
+ const counts = requireRecord(read(summary, "counts"), COUNT_FIELDS, "summary.counts");
125
+ const normalizedCounts = Object.fromEntries(COUNT_FIELDS.map(field => {
126
+ const count = read(counts, field);
127
+ if (!Number.isSafeInteger(count) || count < 0) {
128
+ throw new Error(`Invalid summary.counts.${field}: expected a non-negative integer`);
129
+ }
130
+ return [field, count];
131
+ }));
132
+ return Object.freeze({
133
+ verdict: requireEnum(read(summary, "verdict"), ["in band", "drift detected"], "summary.verdict"),
134
+ counts: Object.freeze(normalizedCounts),
135
+ });
136
+ }
137
+ /**
138
+ * Enforce mode meaning rather than treating it as decorative metadata.
139
+ * @param mode - Validated run mode
140
+ * @param findings - Validated run findings
141
+ */
142
+ function assertModeLayers(mode, findings) {
143
+ const layers = new Set(findings.map(finding => finding.layer));
144
+ if (mode === "deterministic" &&
145
+ (layers.size !== 1 || !layers.has("deterministic"))) {
146
+ throw new Error("Invalid findings: deterministic mode permits deterministic findings only");
147
+ }
148
+ if (mode === "full" &&
149
+ (!layers.has("deterministic") || !layers.has("agentic"))) {
150
+ throw new Error("Invalid findings: full mode requires both deterministic and agentic findings");
151
+ }
152
+ }
153
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/health/contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /** Minimal directory handle used by the production opener and focused tests. */
2
+ export interface DirectorySyncHandle {
3
+ readonly sync: () => Promise<void>;
4
+ readonly close: () => Promise<void>;
5
+ }
6
+ /** Injectable directory opener. */
7
+ export type OpenDirectory = (directory: string, flags: string) => Promise<DirectorySyncHandle>;
8
+ /**
9
+ * Flush a containing directory when the host filesystem supports it.
10
+ * Atomic rename protects ordinary crashes. A successful directory sync adds
11
+ * power-loss durability; known platform-level lack of support is best effort.
12
+ * Permission, device, and other real I/O failures remain fatal.
13
+ * @param directory - Directory containing the renamed target
14
+ * @param openDirectory - Injectable opener used by focused fault tests
15
+ */
16
+ export declare function syncContainingDirectory(directory: string, openDirectory?: OpenDirectory): Promise<void>;
17
+ //# sourceMappingURL=directory-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-sync.d.ts","sourceRoot":"","sources":["../../src/health/directory-sync.ts"],"names":[],"mappings":"AAGA,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,mCAAmC;AACnC,MAAM,MAAM,aAAa,GAAG,CAC1B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,KACV,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAiBlC;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,MAAM,EACjB,aAAa,GAAE,aAAoB,GAClC,OAAO,CAAC,IAAI,CAAC,CAUf"}
@@ -0,0 +1,69 @@
1
+ /** Best-effort directory durability without hiding genuine I/O failures. */
2
+ import { open } from "node:fs/promises";
3
+ const OPEN_UNSUPPORTED = new Set([
4
+ "EISDIR",
5
+ "EINVAL",
6
+ "ENOTSUP",
7
+ "EOPNOTSUPP",
8
+ "ENOSYS",
9
+ ]);
10
+ const SYNC_UNSUPPORTED = new Set([
11
+ "EBADF",
12
+ "EINVAL",
13
+ "ENOTSUP",
14
+ "EOPNOTSUPP",
15
+ "ENOSYS",
16
+ ]);
17
+ /**
18
+ * Flush a containing directory when the host filesystem supports it.
19
+ * Atomic rename protects ordinary crashes. A successful directory sync adds
20
+ * power-loss durability; known platform-level lack of support is best effort.
21
+ * Permission, device, and other real I/O failures remain fatal.
22
+ * @param directory - Directory containing the renamed target
23
+ * @param openDirectory - Injectable opener used by focused fault tests
24
+ */
25
+ export async function syncContainingDirectory(directory, openDirectory = open) {
26
+ const handle = await tryOpenDirectory(directory, openDirectory);
27
+ if (handle === undefined)
28
+ return;
29
+ try {
30
+ await handle.sync();
31
+ }
32
+ catch (error) {
33
+ if (!hasCode(error, SYNC_UNSUPPORTED))
34
+ throw error;
35
+ }
36
+ finally {
37
+ await handle.close();
38
+ }
39
+ }
40
+ /**
41
+ * Open a directory or identify a host that does not expose directory opens.
42
+ * @param directory - Directory to open
43
+ * @param openDirectory - Platform directory opener
44
+ * @returns Open handle, or undefined when directory opens are unsupported
45
+ */
46
+ async function tryOpenDirectory(directory, openDirectory) {
47
+ try {
48
+ return await openDirectory(directory, "r");
49
+ }
50
+ catch (error) {
51
+ if (hasCode(error, OPEN_UNSUPPORTED))
52
+ return undefined;
53
+ throw error;
54
+ }
55
+ }
56
+ /**
57
+ * Match a Node-style error code against a closed unsupported-code set.
58
+ * @param error - Candidate Node-style error
59
+ * @param codes - Closed set of recognized codes
60
+ * @returns Whether the error carries a recognized code
61
+ */
62
+ function hasCode(error, codes) {
63
+ return (typeof error === "object" &&
64
+ error !== null &&
65
+ "code" in error &&
66
+ typeof error.code === "string" &&
67
+ codes.has(error.code));
68
+ }
69
+ //# sourceMappingURL=directory-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-sync.js","sourceRoot":"","sources":["../../src/health/directory-sync.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAcxC,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC/B,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,YAAY;IACZ,QAAQ;CACT,CAAC,CAAC;AACH,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC/B,OAAO;IACP,QAAQ;IACR,SAAS;IACT,YAAY;IACZ,QAAQ;CACT,CAAC,CAAC;AAEH;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,SAAiB,EACjB,gBAA+B,IAAI;IAEnC,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;IAChE,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO;IACjC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;IACtB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,gBAAgB,CAAC;YAAE,MAAM,KAAK,CAAC;IACrD,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,gBAAgB,CAC7B,SAAiB,EACjB,aAA4B;IAE5B,IAAI,CAAC;QACH,OAAO,MAAM,aAAa,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,OAAO,CAAC,KAAK,EAAE,gBAAgB,CAAC;YAAE,OAAO,SAAS,CAAC;QACvD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,OAAO,CAAC,KAAc,EAAE,KAA0B;IACzD,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,MAAM,IAAI,KAAK;QACf,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;QAC9B,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CACtB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from "./contract.js";
2
+ export * from "./storage.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/health/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from "./contract.js";
2
+ export * from "./storage.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/health/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { type HealthResult } from "./contract.js";
2
+ export declare const HEALTH_RESULT_PATH = ".lisa/health/latest.json";
3
+ export declare const MAX_HEALTH_RESULT_BYTES: number;
4
+ /** Non-throwing read outcome for persisted runtime health state. */
5
+ export type HealthReadResult = {
6
+ readonly status: "available";
7
+ readonly result: HealthResult;
8
+ readonly lastRun: string;
9
+ } | {
10
+ readonly status: "never-run";
11
+ } | {
12
+ readonly status: "unreadable";
13
+ readonly reason: string;
14
+ };
15
+ /** Atomic write or monotonic no-op outcome. */
16
+ export type HealthWriteResult = {
17
+ readonly status: "written";
18
+ readonly path: string;
19
+ readonly result: HealthResult;
20
+ } | {
21
+ readonly status: "unchanged";
22
+ readonly path: string;
23
+ readonly result: HealthResult;
24
+ };
25
+ /**
26
+ * Read the canonical latest result without throwing for runtime-state faults.
27
+ * @param projectRoot - Project containing runtime health state
28
+ * @returns Available, never-run, or unreadable outcome
29
+ */
30
+ export declare function readLatestHealthResult(projectRoot: string): Promise<HealthReadResult>;
31
+ /**
32
+ * Validate and atomically publish a completed result. Writes are serialized,
33
+ * and an older or equal completion timestamp cannot replace a newer result.
34
+ * @param projectRoot - Project receiving runtime health state
35
+ * @param candidate - Untrusted completed result candidate
36
+ * @returns Written or monotonic unchanged outcome
37
+ */
38
+ export declare function writeLatestHealthResult(projectRoot: string, candidate: unknown): Promise<HealthWriteResult>;
39
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/health/storage.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,YAAY,EAAwB,MAAM,eAAe,CAAC;AAGxE,eAAO,MAAM,kBAAkB,6BAA6B,CAAC;AAC7D,eAAO,MAAM,uBAAuB,QAAa,CAAC;AAElD,oEAAoE;AACpE,MAAM,MAAM,gBAAgB,GACxB;IACE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,GACD;IAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GAChC;IAAE,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/D,+CAA+C;AAC/C,MAAM,MAAM,iBAAiB,GACzB;IACE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B,GACD;IACE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B,CAAC;AAEN;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAkB3B;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,OAAO,GACjB,OAAO,CAAC,iBAAiB,CAAC,CA4C5B"}
@@ -0,0 +1,262 @@
1
+ /** Confined, bounded, atomic storage for the latest completed Health v1 run. */
2
+ import * as fse from "fs-extra";
3
+ import { lstat, open, realpath, rename, rm } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { withFileTargetLock } from "../core/learnings-lock.js";
6
+ import { validateHealthResult } from "./contract.js";
7
+ import { syncContainingDirectory } from "./directory-sync.js";
8
+ export const HEALTH_RESULT_PATH = ".lisa/health/latest.json";
9
+ export const MAX_HEALTH_RESULT_BYTES = 256 * 1024;
10
+ /**
11
+ * Read the canonical latest result without throwing for runtime-state faults.
12
+ * @param projectRoot - Project containing runtime health state
13
+ * @returns Available, never-run, or unreadable outcome
14
+ */
15
+ export async function readLatestHealthResult(projectRoot) {
16
+ try {
17
+ const target = await resolveReadTarget(projectRoot);
18
+ const payload = await readBoundedRegularFile(target);
19
+ if (payload === undefined)
20
+ return Object.freeze({ status: "never-run" });
21
+ const parsed = JSON.parse(payload);
22
+ const result = validateHealthResult(parsed);
23
+ return Object.freeze({
24
+ status: "available",
25
+ result,
26
+ lastRun: result.completedAt,
27
+ });
28
+ }
29
+ catch (error) {
30
+ return Object.freeze({
31
+ status: "unreadable",
32
+ reason: safeReadReason(error),
33
+ });
34
+ }
35
+ }
36
+ /**
37
+ * Validate and atomically publish a completed result. Writes are serialized,
38
+ * and an older or equal completion timestamp cannot replace a newer result.
39
+ * @param projectRoot - Project receiving runtime health state
40
+ * @param candidate - Untrusted completed result candidate
41
+ * @returns Written or monotonic unchanged outcome
42
+ */
43
+ export async function writeLatestHealthResult(projectRoot, candidate) {
44
+ const result = validateHealthResult(candidate);
45
+ const serialized = serializeHealthResult(result);
46
+ const { directory, target } = await resolveWriteTarget(projectRoot);
47
+ return withFileTargetLock(target, async () => {
48
+ await assertSafeTarget(target);
49
+ const current = await readLatestHealthResult(projectRoot);
50
+ if (current.status === "available") {
51
+ const samePayload = serializeHealthResult(current.result) === serialized;
52
+ if (current.result.runId === result.runId) {
53
+ if (!samePayload) {
54
+ throw new Error("Health result conflict: runId was reused");
55
+ }
56
+ return unchanged(target, current.result);
57
+ }
58
+ if (current.result.completedAt === result.completedAt) {
59
+ throw new Error("Health result conflict: completedAt is shared by different runs");
60
+ }
61
+ if (current.result.completedAt > result.completedAt) {
62
+ return unchanged(target, current.result);
63
+ }
64
+ }
65
+ const temporary = path.join(directory, `.${path.basename(target)}.${process.pid}.${crypto.randomUUID()}.tmp`);
66
+ try {
67
+ const handle = await open(temporary, "wx", 0o600);
68
+ try {
69
+ await handle.writeFile(serialized, "utf8");
70
+ await handle.sync();
71
+ }
72
+ finally {
73
+ await handle.close();
74
+ }
75
+ await assertSafeTarget(target);
76
+ await rename(temporary, target);
77
+ await syncContainingDirectory(directory);
78
+ }
79
+ finally {
80
+ await rm(temporary, { force: true });
81
+ }
82
+ return Object.freeze({ status: "written", path: target, result });
83
+ });
84
+ }
85
+ /**
86
+ * Serialize once and reject over-budget output before filesystem mutation.
87
+ * @param result - Validated result
88
+ * @returns Canonical bounded JSON payload
89
+ */
90
+ function serializeHealthResult(result) {
91
+ const serialized = `${JSON.stringify(result, null, 2)}\n`;
92
+ if (Buffer.byteLength(serialized, "utf8") > MAX_HEALTH_RESULT_BYTES) {
93
+ throw new Error("Health result exceeds 256 KiB");
94
+ }
95
+ return serialized;
96
+ }
97
+ /**
98
+ * Return the canonical idempotent no-write result.
99
+ * @param target - Canonical result path
100
+ * @param result - Existing persisted result
101
+ * @returns Frozen unchanged outcome
102
+ */
103
+ function unchanged(target, result) {
104
+ return Object.freeze({ status: "unchanged", path: target, result });
105
+ }
106
+ /**
107
+ * Resolve an existing project root and canonical read target.
108
+ * @param projectRoot - Project containing runtime health state
109
+ * @returns Confined canonical target
110
+ */
111
+ async function resolveReadTarget(projectRoot) {
112
+ const root = await requireProjectRoot(projectRoot);
113
+ const lisaDirectory = path.join(root, ".lisa");
114
+ const healthDirectory = path.join(lisaDirectory, "health");
115
+ if ((await statPath(lisaDirectory)) !== undefined) {
116
+ await assertConfinedDirectory(root, lisaDirectory);
117
+ }
118
+ if ((await statPath(healthDirectory)) !== undefined) {
119
+ await assertConfinedDirectory(root, healthDirectory);
120
+ }
121
+ return path.join(root, HEALTH_RESULT_PATH);
122
+ }
123
+ /**
124
+ * Create safe canonical parent directories and return write paths.
125
+ * @param projectRoot - Project receiving runtime health state
126
+ * @returns Confined parent directory and target
127
+ */
128
+ async function resolveWriteTarget(projectRoot) {
129
+ const root = await requireProjectRoot(projectRoot);
130
+ const lisaDirectory = path.join(root, ".lisa");
131
+ const directory = path.join(lisaDirectory, "health");
132
+ await ensureSafeDirectory(root, lisaDirectory);
133
+ await ensureSafeDirectory(root, directory);
134
+ return { directory, target: path.join(directory, "latest.json") };
135
+ }
136
+ /**
137
+ * Resolve and verify a real directory root.
138
+ * @param projectRoot - Project-root candidate
139
+ * @returns Canonical real project root
140
+ */
141
+ async function requireProjectRoot(projectRoot) {
142
+ const resolved = path.resolve(projectRoot);
143
+ const root = await realpath(resolved);
144
+ const metadata = await lstat(root);
145
+ if (!metadata.isDirectory()) {
146
+ throw new Error("Health project root is not a directory");
147
+ }
148
+ return root;
149
+ }
150
+ /**
151
+ * Create one directory only after its nearest existing parent stays confined.
152
+ * @param root - Canonical project root
153
+ * @param directory - Intended child directory
154
+ */
155
+ async function ensureSafeDirectory(root, directory) {
156
+ if ((await statPath(directory)) !== undefined) {
157
+ await assertConfinedDirectory(root, directory);
158
+ return;
159
+ }
160
+ await assertConfinedDirectory(root, path.dirname(directory));
161
+ await fse.ensureDir(directory);
162
+ await assertConfinedDirectory(root, directory);
163
+ }
164
+ /**
165
+ * Reject symlinked, special, or root-escaping storage parents.
166
+ * @param root - Canonical project root
167
+ * @param directory - Existing storage parent
168
+ */
169
+ async function assertConfinedDirectory(root, directory) {
170
+ const metadata = await lstat(directory);
171
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
172
+ throw new Error("Unsafe health storage parent");
173
+ }
174
+ const actual = await realpath(directory);
175
+ if (actual !== root && !actual.startsWith(`${root}${path.sep}`)) {
176
+ throw new Error("Unsafe health storage parent escapes project root");
177
+ }
178
+ }
179
+ /**
180
+ * Reject an existing target unless it is a regular non-symlink file.
181
+ * @param target - Canonical result target
182
+ */
183
+ async function assertSafeTarget(target) {
184
+ try {
185
+ const metadata = await lstat(target);
186
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
187
+ throw new Error("Unsafe health result target");
188
+ }
189
+ }
190
+ catch (error) {
191
+ if (error.code !== "ENOENT")
192
+ throw error;
193
+ }
194
+ }
195
+ /**
196
+ * Read a bounded stable regular-file inode.
197
+ * @param target - Canonical result target
198
+ * @returns Strict UTF-8 payload, or undefined when absent
199
+ */
200
+ async function readBoundedRegularFile(target) {
201
+ try {
202
+ const before = await lstat(target);
203
+ if (!before.isFile() || before.isSymbolicLink()) {
204
+ throw new Error("Unsafe health result target");
205
+ }
206
+ if (before.size > MAX_HEALTH_RESULT_BYTES) {
207
+ throw new Error("Health result exceeds 256 KiB");
208
+ }
209
+ const handle = await open(target, "r");
210
+ try {
211
+ const opened = await handle.stat();
212
+ if (!opened.isFile() ||
213
+ opened.dev !== before.dev ||
214
+ opened.ino !== before.ino ||
215
+ opened.size > MAX_HEALTH_RESULT_BYTES) {
216
+ throw new Error("Health result changed during read");
217
+ }
218
+ const bytes = await handle.readFile();
219
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
220
+ }
221
+ finally {
222
+ await handle.close();
223
+ }
224
+ }
225
+ catch (error) {
226
+ if (error.code === "ENOENT")
227
+ return undefined;
228
+ throw error;
229
+ }
230
+ }
231
+ /**
232
+ * Read path metadata without following symlinks.
233
+ * @param filePath - Filesystem path
234
+ * @returns Metadata, or undefined when absent
235
+ */
236
+ async function statPath(filePath) {
237
+ try {
238
+ return await lstat(filePath);
239
+ }
240
+ catch (error) {
241
+ if (error.code === "ENOENT")
242
+ return undefined;
243
+ throw error;
244
+ }
245
+ }
246
+ /**
247
+ * Reduce runtime-state failures to a non-sensitive operator-readable reason.
248
+ * @param error - Read failure
249
+ * @returns Stable operator-facing reason
250
+ */
251
+ function safeReadReason(error) {
252
+ const message = error instanceof Error ? error.message : "unknown error";
253
+ if (message.includes("schemaVersion"))
254
+ return "unsupported health result schema";
255
+ if (message.includes("256 KiB"))
256
+ return "health result exceeds size limit";
257
+ if (message.includes("Unsafe") || message.includes("changed during read")) {
258
+ return "unsafe health result storage";
259
+ }
260
+ return "malformed health result";
261
+ }
262
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/health/storage.ts"],"names":[],"mappings":"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"}