@codyswann/lisa 2.217.8 → 2.219.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.
- package/all/create-only/.claude/rules/PROJECT_LEARNINGS.md +7 -0
- package/dist/core/learnings-contract.d.ts +38 -0
- package/dist/core/learnings-contract.d.ts.map +1 -0
- package/dist/core/learnings-contract.js +36 -0
- package/dist/core/learnings-contract.js.map +1 -0
- package/dist/core/learnings-entry.d.ts +9 -0
- package/dist/core/learnings-entry.d.ts.map +1 -0
- package/dist/core/learnings-entry.js +233 -0
- package/dist/core/learnings-entry.js.map +1 -0
- package/dist/core/learnings-file-safety.d.ts +23 -0
- package/dist/core/learnings-file-safety.d.ts.map +1 -0
- package/dist/core/learnings-file-safety.js +84 -0
- package/dist/core/learnings-file-safety.js.map +1 -0
- package/dist/core/learnings-lock.d.ts +8 -0
- package/dist/core/learnings-lock.d.ts.map +1 -0
- package/dist/core/learnings-lock.js +227 -0
- package/dist/core/learnings-lock.js.map +1 -0
- package/dist/core/learnings-writer.d.ts +22 -0
- package/dist/core/learnings-writer.d.ts.map +1 -0
- package/dist/core/learnings-writer.js +128 -0
- package/dist/core/learnings-writer.js.map +1 -0
- package/dist/core/learnings.d.ts +4 -0
- package/dist/core/learnings.d.ts.map +1 -0
- package/dist/core/learnings.js +4 -0
- package/dist/core/learnings.js.map +1 -0
- package/dist/core/lisa.d.ts +26 -0
- package/dist/core/lisa.d.ts.map +1 -1
- package/dist/core/lisa.js +82 -8
- package/dist/core/lisa.js.map +1 -1
- package/dist/core/project-config.d.ts +19 -0
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +87 -14
- package/dist/core/project-config.js.map +1 -1
- package/dist/strategies/create-only.d.ts +10 -0
- package/dist/strategies/create-only.d.ts.map +1 -1
- package/dist/strategies/create-only.js +27 -7
- package/dist/strategies/create-only.js.map +1 -1
- package/dist/sync/registry.d.ts +0 -12
- package/dist/sync/registry.d.ts.map +1 -1
- package/dist/sync/registry.js +18 -0
- package/dist/sync/registry.js.map +1 -1
- package/package.json +2 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-debrief-apply/SKILL.md +3 -3
- package/plugins/lisa/rules/eager/config-resolution.md +9 -0
- package/plugins/lisa/rules/reference/config-resolution.md +2 -0
- package/plugins/lisa/skills/lisa-debrief-apply/SKILL.md +3 -3
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-debrief-apply/SKILL.md +3 -3
- 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-copilot/rules/eager/config-resolution.md +9 -0
- package/plugins/lisa-copilot/rules/reference/config-resolution.md +2 -0
- package/plugins/lisa-copilot/skills/lisa-debrief-apply/SKILL.md +3 -3
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +2 -0
- package/plugins/lisa-cursor/rules/config-resolution.mdc +9 -0
- package/plugins/lisa-cursor/skills/lisa-debrief-apply/SKILL.md +3 -3
- 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/plugins/src/base/rules/eager/config-resolution.md +9 -0
- package/plugins/src/base/rules/reference/config-resolution.md +2 -0
- package/plugins/src/base/skills/lisa-debrief-apply/SKILL.md +3 -3
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/** Cross-process serialization for project learnings writes. */
|
|
2
|
+
import { link, lstat, readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
const MAX_LOCK_ATTEMPTS = 200;
|
|
4
|
+
const LOCK_RETRY_DELAY_MS = 10;
|
|
5
|
+
const STALE_LOCK_MS = 30_000;
|
|
6
|
+
/**
|
|
7
|
+
* Serialize same-target writers across processes with an exclusive hard link.
|
|
8
|
+
* @param target - Absolute learnings target
|
|
9
|
+
* @param operation - Complete read, validate, and atomic-write transaction
|
|
10
|
+
* @returns Operation result
|
|
11
|
+
*/
|
|
12
|
+
export async function withLearningTargetLock(target, operation) {
|
|
13
|
+
const lockPath = `${target}.lock`;
|
|
14
|
+
const owner = {
|
|
15
|
+
token: crypto.randomUUID(),
|
|
16
|
+
pid: process.pid,
|
|
17
|
+
createdAt: Date.now(),
|
|
18
|
+
};
|
|
19
|
+
const lease = await acquireLock(lockPath, owner, 0);
|
|
20
|
+
try {
|
|
21
|
+
return await operation();
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
await releaseLock(lockPath, lease);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Publish complete owner metadata atomically via a hard link.
|
|
29
|
+
* @param lockPath - Shared lock path
|
|
30
|
+
* @param owner - Unique owner metadata
|
|
31
|
+
* @param attempt - Current retry count
|
|
32
|
+
* @returns Acquired lock lease
|
|
33
|
+
*/
|
|
34
|
+
async function acquireLock(lockPath, owner, attempt) {
|
|
35
|
+
const ownerPath = `${lockPath}.${owner.token}.owner`;
|
|
36
|
+
await writeFile(ownerPath, JSON.stringify(owner), {
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
flag: "wx",
|
|
39
|
+
});
|
|
40
|
+
const acquired = await publishOwnerLink(ownerPath, lockPath);
|
|
41
|
+
if (acquired) {
|
|
42
|
+
return { owner, ownerPath };
|
|
43
|
+
}
|
|
44
|
+
await removeFileIfPresent(ownerPath);
|
|
45
|
+
if (attempt >= MAX_LOCK_ATTEMPTS) {
|
|
46
|
+
throw new Error(`Timed out waiting for learnings lock: ${lockPath}`);
|
|
47
|
+
}
|
|
48
|
+
await reclaimStaleLock(lockPath);
|
|
49
|
+
await delay(LOCK_RETRY_DELAY_MS);
|
|
50
|
+
return acquireLock(lockPath, owner, attempt + 1);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Try to hard-link fully written owner metadata into the lock path.
|
|
54
|
+
* @param ownerPath - Fully written owner metadata file
|
|
55
|
+
* @param lockPath - Destination lock path
|
|
56
|
+
* @returns Whether publication acquired the lock
|
|
57
|
+
*/
|
|
58
|
+
async function publishOwnerLink(ownerPath, lockPath) {
|
|
59
|
+
try {
|
|
60
|
+
await link(ownerPath, lockPath);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error.code === "EEXIST" ||
|
|
65
|
+
error.code === "ENOENT") {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Remove only the lock inode still linked to this lease's owner file.
|
|
73
|
+
* @param lockPath - Shared lock path
|
|
74
|
+
* @param lease - Owner lease retaining the same inode
|
|
75
|
+
*/
|
|
76
|
+
async function releaseLock(lockPath, lease) {
|
|
77
|
+
try {
|
|
78
|
+
if (await sameFile(lockPath, lease.ownerPath)) {
|
|
79
|
+
await unlink(lockPath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await removeFileIfPresent(lease.ownerPath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Reclaim an expired lock only when its declared process is no longer live.
|
|
88
|
+
* @param lockPath - Shared lock path
|
|
89
|
+
*/
|
|
90
|
+
async function reclaimStaleLock(lockPath) {
|
|
91
|
+
const before = await statFile(lockPath);
|
|
92
|
+
if (before === undefined || !before.isFile()) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const owner = await readLockOwner(lockPath);
|
|
96
|
+
const timestamp = owner?.createdAt ?? Number(before.mtimeMs);
|
|
97
|
+
if (Date.now() - timestamp <= STALE_LOCK_MS ||
|
|
98
|
+
(owner !== undefined && isProcessLive(owner.pid))) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const retainedOwnerPath = owner === undefined ? undefined : `${lockPath}.${owner.token}.owner`;
|
|
102
|
+
const quarantine = retainedOwnerPath !== undefined &&
|
|
103
|
+
(await sameFile(lockPath, retainedOwnerPath))
|
|
104
|
+
? retainedOwnerPath
|
|
105
|
+
: `${lockPath}.${crypto.randomUUID()}.stale`;
|
|
106
|
+
const linked = quarantine === retainedOwnerPath
|
|
107
|
+
? true
|
|
108
|
+
: await publishOwnerLink(lockPath, quarantine);
|
|
109
|
+
if (!linked) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
if (await sameFile(lockPath, quarantine)) {
|
|
114
|
+
await removeFileIfPresent(lockPath);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
await removeFileIfPresent(quarantine);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Read bounded owner metadata; partial or special lock files are unowned.
|
|
123
|
+
* @param lockPath - Shared lock path
|
|
124
|
+
* @returns Parsed owner or undefined
|
|
125
|
+
*/
|
|
126
|
+
async function readLockOwner(lockPath) {
|
|
127
|
+
const metadata = await statFile(lockPath);
|
|
128
|
+
if (metadata === undefined || !metadata.isFile() || metadata.size > 512) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const parsed = JSON.parse(await readFile(lockPath, "utf8"));
|
|
133
|
+
return isLockOwner(parsed) ? parsed : undefined;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Compare two regular-file paths by device and inode.
|
|
141
|
+
* @param left - First file path
|
|
142
|
+
* @param right - Second file path
|
|
143
|
+
* @returns Whether both paths retain the same regular-file inode
|
|
144
|
+
*/
|
|
145
|
+
async function sameFile(left, right) {
|
|
146
|
+
const [leftStat, rightStat] = await Promise.all([
|
|
147
|
+
statFile(left),
|
|
148
|
+
statFile(right),
|
|
149
|
+
]);
|
|
150
|
+
return (leftStat !== undefined &&
|
|
151
|
+
rightStat !== undefined &&
|
|
152
|
+
leftStat.isFile() &&
|
|
153
|
+
rightStat.isFile() &&
|
|
154
|
+
leftStat.dev === rightStat.dev &&
|
|
155
|
+
leftStat.ino === rightStat.ino);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Read file metadata without treating an absent path as an error.
|
|
159
|
+
* @param filePath - Filesystem path
|
|
160
|
+
* @returns File metadata or undefined
|
|
161
|
+
*/
|
|
162
|
+
async function statFile(filePath) {
|
|
163
|
+
try {
|
|
164
|
+
return await lstat(filePath);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error.code === "ENOENT") {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Remove one path without recursive deletion.
|
|
175
|
+
* @param filePath - Regular file or hard-link path
|
|
176
|
+
*/
|
|
177
|
+
async function removeFileIfPresent(filePath) {
|
|
178
|
+
try {
|
|
179
|
+
await unlink(filePath);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (error.code !== "ENOENT") {
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Narrow parsed lock metadata to the exact ownership shape.
|
|
189
|
+
* @param value - Parsed metadata
|
|
190
|
+
* @returns Whether the metadata is a lock owner
|
|
191
|
+
*/
|
|
192
|
+
function isLockOwner(value) {
|
|
193
|
+
if (value === null || typeof value !== "object") {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
const owner = value;
|
|
197
|
+
return (typeof owner.token === "string" &&
|
|
198
|
+
typeof owner.pid === "number" &&
|
|
199
|
+
Number.isSafeInteger(owner.pid) &&
|
|
200
|
+
owner.pid > 0 &&
|
|
201
|
+
typeof owner.createdAt === "number" &&
|
|
202
|
+
Number.isSafeInteger(owner.createdAt));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Treat permission-denied PID probes as live and missing PIDs as dead.
|
|
206
|
+
* @param pid - Declared owner process id
|
|
207
|
+
* @returns Whether the process may still be alive
|
|
208
|
+
*/
|
|
209
|
+
function isProcessLive(pid) {
|
|
210
|
+
try {
|
|
211
|
+
process.kill(pid, 0);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
return error.code !== "ESRCH";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Await a short retry delay without blocking the event loop.
|
|
220
|
+
* @param milliseconds - Delay duration
|
|
221
|
+
*/
|
|
222
|
+
async function delay(milliseconds) {
|
|
223
|
+
await new Promise(resolve => {
|
|
224
|
+
setTimeout(resolve, milliseconds);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=learnings-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"learnings-lock.js","sourceRoot":"","sources":["../../src/core/learnings-lock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type LearningEntry } from "./learnings-contract.js";
|
|
2
|
+
/**
|
|
3
|
+
* Persist one already-selected learning without generating or truncating it.
|
|
4
|
+
* @param projectRoot - Absolute path to the host project root
|
|
5
|
+
* @param candidate - Untrusted seven-field learning entry
|
|
6
|
+
* @returns Absolute path to the persisted learnings file
|
|
7
|
+
*/
|
|
8
|
+
export declare function persistLearningEntry(projectRoot: string, candidate: unknown): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Render the complete file in one stable, machine-readable Markdown form.
|
|
11
|
+
* @param entries - Validated entries to serialize
|
|
12
|
+
* @returns Canonical Markdown plus JSONL document
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderLearningsFile(entries: readonly LearningEntry[]): string;
|
|
15
|
+
/**
|
|
16
|
+
* Parse and revalidate an existing file before it participates in a write.
|
|
17
|
+
* @param content - Existing learnings document
|
|
18
|
+
* @returns Revalidated entries from the document
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseLearningsFile(content: string): LearningEntry[];
|
|
21
|
+
export { validateLearningEntry } from "./learnings-entry.js";
|
|
22
|
+
//# sourceMappingURL=learnings-writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"learnings-writer.d.ts","sourceRoot":"","sources":["../../src/core/learnings-writer.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,yBAAyB,CAAC;AAqBjC;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,OAAO,GACjB,OAAO,CAAC,MAAM,CAAC,CA6BjB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,GAAG,MAAM,CAG7E;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,EAAE,CAsBnE;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** Deterministic persistence for project-local learnings. */
|
|
2
|
+
import * as fse from "fs-extra";
|
|
3
|
+
import { rename, writeFile } from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { LEARNINGS_CONTRACT, estimateLearningTokens, } from "./learnings-contract.js";
|
|
6
|
+
import { validateLearningEntry } from "./learnings-entry.js";
|
|
7
|
+
import { assertSafeLearningParents, readExistingLearnings, resolveSafeLearningTarget, } from "./learnings-file-safety.js";
|
|
8
|
+
import { withLearningTargetLock } from "./learnings-lock.js";
|
|
9
|
+
import { readProjectConfig, resolveProjectLearningsFile, } from "./project-config.js";
|
|
10
|
+
const FILE_HEADER = `# Project Learnings
|
|
11
|
+
|
|
12
|
+
<!-- lisa-learnings-contract:v${LEARNINGS_CONTRACT.version} -->
|
|
13
|
+
|
|
14
|
+
`;
|
|
15
|
+
const JSON_FENCE_START = "```jsonl\n";
|
|
16
|
+
const JSON_FENCE_END = "\n```\n";
|
|
17
|
+
/**
|
|
18
|
+
* Persist one already-selected learning without generating or truncating it.
|
|
19
|
+
* @param projectRoot - Absolute path to the host project root
|
|
20
|
+
* @param candidate - Untrusted seven-field learning entry
|
|
21
|
+
* @returns Absolute path to the persisted learnings file
|
|
22
|
+
*/
|
|
23
|
+
export async function persistLearningEntry(projectRoot, candidate) {
|
|
24
|
+
const entry = validateLearningEntry(candidate);
|
|
25
|
+
const initialDocument = buildNextDocument([], entry);
|
|
26
|
+
const config = await readProjectConfig(projectRoot);
|
|
27
|
+
const relativeFile = resolveProjectLearningsFile(config);
|
|
28
|
+
const { root, target } = resolveSafeLearningTarget(projectRoot, relativeFile);
|
|
29
|
+
await assertSafeLearningParents(root, path.dirname(target));
|
|
30
|
+
await fse.ensureDir(path.dirname(target));
|
|
31
|
+
return withLearningTargetLock(target, async () => {
|
|
32
|
+
await assertSafeLearningParents(root, path.dirname(target));
|
|
33
|
+
const existing = await readExistingLearnings(target);
|
|
34
|
+
const entries = existing === undefined ? [] : parseLearningsFile(existing);
|
|
35
|
+
const rendered = existing === undefined
|
|
36
|
+
? initialDocument
|
|
37
|
+
: buildNextDocument(entries, entry);
|
|
38
|
+
const temporary = path.join(path.dirname(target), `.${path.basename(target)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
39
|
+
try {
|
|
40
|
+
await writeFile(temporary, rendered, { encoding: "utf8", flag: "wx" });
|
|
41
|
+
await assertSafeLearningParents(root, path.dirname(target));
|
|
42
|
+
await rename(temporary, target);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await fse.remove(temporary);
|
|
46
|
+
}
|
|
47
|
+
return target;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Render the complete file in one stable, machine-readable Markdown form.
|
|
52
|
+
* @param entries - Validated entries to serialize
|
|
53
|
+
* @returns Canonical Markdown plus JSONL document
|
|
54
|
+
*/
|
|
55
|
+
export function renderLearningsFile(entries) {
|
|
56
|
+
const lines = entries.map(entry => JSON.stringify(entry)).join("\n");
|
|
57
|
+
return `${FILE_HEADER}${JSON_FENCE_START}${lines}${JSON_FENCE_END}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse and revalidate an existing file before it participates in a write.
|
|
61
|
+
* @param content - Existing learnings document
|
|
62
|
+
* @returns Revalidated entries from the document
|
|
63
|
+
*/
|
|
64
|
+
export function parseLearningsFile(content) {
|
|
65
|
+
if (estimateLearningTokens(content) > LEARNINGS_CONTRACT.maxTokens) {
|
|
66
|
+
throw new Error(`Project learnings payload exceeds maxTokens ${LEARNINGS_CONTRACT.maxTokens}`);
|
|
67
|
+
}
|
|
68
|
+
if (!content.startsWith(`${FILE_HEADER}${JSON_FENCE_START}`) ||
|
|
69
|
+
!content.endsWith(JSON_FENCE_END)) {
|
|
70
|
+
throw new Error("Invalid project learnings file format");
|
|
71
|
+
}
|
|
72
|
+
const jsonStart = FILE_HEADER.length + JSON_FENCE_START.length;
|
|
73
|
+
const jsonEnd = content.length - JSON_FENCE_END.length;
|
|
74
|
+
const entries = parseJsonLines(content.slice(jsonStart, jsonEnd)).map(validateLearningEntry);
|
|
75
|
+
if (new Set(entries.map(entry => entry.id)).size !== entries.length) {
|
|
76
|
+
throw new Error("Invalid project learnings payload: duplicate ids");
|
|
77
|
+
}
|
|
78
|
+
assertDocumentBudget(content, entries.length, "Project learnings payload");
|
|
79
|
+
return entries;
|
|
80
|
+
}
|
|
81
|
+
export { validateLearningEntry } from "./learnings-entry.js";
|
|
82
|
+
/**
|
|
83
|
+
* Build and budget-check the next canonical document.
|
|
84
|
+
* @param entries - Existing validated entries
|
|
85
|
+
* @param entry - New validated entry
|
|
86
|
+
* @returns Next canonical document
|
|
87
|
+
*/
|
|
88
|
+
function buildNextDocument(entries, entry) {
|
|
89
|
+
if (entries.some(current => current.id === entry.id)) {
|
|
90
|
+
throw new Error(`Duplicate learning id: ${entry.id}`);
|
|
91
|
+
}
|
|
92
|
+
const nextEntries = [...entries, entry].sort((left, right) => left.id.localeCompare(right.id));
|
|
93
|
+
const rendered = renderLearningsFile(nextEntries);
|
|
94
|
+
assertDocumentBudget(rendered, nextEntries.length, "Learnings file");
|
|
95
|
+
return rendered;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Enforce the shared entry-count and model-agnostic token upper bounds.
|
|
99
|
+
* @param content - Canonical document
|
|
100
|
+
* @param entryCount - Number of entries in the document
|
|
101
|
+
* @param context - Error-message prefix
|
|
102
|
+
*/
|
|
103
|
+
function assertDocumentBudget(content, entryCount, context) {
|
|
104
|
+
if (entryCount > LEARNINGS_CONTRACT.maxEntries) {
|
|
105
|
+
throw new Error(`${context} exceeds maxEntries ${LEARNINGS_CONTRACT.maxEntries}`);
|
|
106
|
+
}
|
|
107
|
+
const estimatedTokens = estimateLearningTokens(content);
|
|
108
|
+
if (estimatedTokens > LEARNINGS_CONTRACT.maxTokens) {
|
|
109
|
+
throw new Error(`${context} exceeds maxTokens ${LEARNINGS_CONTRACT.maxTokens} (measured ${estimatedTokens})`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Parse the strict JSONL payload with a stable, user-facing error.
|
|
114
|
+
* @param payload - JSONL payload
|
|
115
|
+
* @returns Parsed unknown entry values
|
|
116
|
+
*/
|
|
117
|
+
function parseJsonLines(payload) {
|
|
118
|
+
if (payload === "") {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return payload.split("\n").map(line => JSON.parse(line));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw new Error("Invalid project learnings JSONL payload");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=learnings-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"learnings-writer.js","sourceRoot":"","sources":["../../src/core/learnings-writer.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,OAAO,KAAK,GAAG,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EACL,kBAAkB,EAClB,sBAAsB,GAEvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,yBAAyB,GAC1B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EACL,iBAAiB,EACjB,2BAA2B,GAC5B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,GAAG;;gCAEY,kBAAkB,CAAC,OAAO;;CAEzD,CAAC;AACF,MAAM,gBAAgB,GAAG,YAAY,CAAC;AACtC,MAAM,cAAc,GAAG,SAAS,CAAC;AAEjC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,WAAmB,EACnB,SAAkB;IAElB,MAAM,KAAK,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;IAC/C,MAAM,eAAe,GAAG,iBAAiB,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,WAAW,CAAC,CAAC;IACpD,MAAM,YAAY,GAAG,2BAA2B,CAAC,MAAM,CAAC,CAAC;IACzD,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,yBAAyB,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;IAC9E,MAAM,yBAAyB,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5D,MAAM,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAC1C,OAAO,sBAAsB,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE;QAC/C,MAAM,yBAAyB,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAC5D,MAAM,QAAQ,GAAG,MAAM,qBAAqB,CAAC,MAAM,CAAC,CAAC;QACrD,MAAM,OAAO,GAAG,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC;QAC3E,MAAM,QAAQ,GACZ,QAAQ,KAAK,SAAS;YACpB,CAAC,CAAC,eAAe;YACjB,CAAC,CAAC,iBAAiB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACxC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CACzB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EACpB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,GAAG,IAAI,MAAM,CAAC,UAAU,EAAE,MAAM,CACtE,CAAC;QACF,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACvE,MAAM,yBAAyB,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;YAC5D,MAAM,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAClC,CAAC;gBAAS,CAAC;YACT,MAAM,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC9B,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAiC;IACnE,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrE,OAAO,GAAG,WAAW,GAAG,gBAAgB,GAAG,KAAK,GAAG,cAAc,EAAE,CAAC;AACtE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe;IAChD,IAAI,sBAAsB,CAAC,OAAO,CAAC,GAAG,kBAAkB,CAAC,SAAS,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CACb,+CAA+C,kBAAkB,CAAC,SAAS,EAAE,CAC9E,CAAC;IACJ,CAAC;IACD,IACE,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,WAAW,GAAG,gBAAgB,EAAE,CAAC;QACxD,CAAC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,EACjC,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IACD,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC;IAC/D,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC;IACvD,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CACnE,qBAAqB,CACtB,CAAC;IACF,IAAI,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC;QACpE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACtE,CAAC;IACD,oBAAoB,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC;IAC3E,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE7D;;;;;GAKG;AACH,SAAS,iBAAiB,CACxB,OAAiC,EACjC,KAAoB;IAEpB,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,0BAA0B,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,MAAM,WAAW,GAAG,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAC3D,IAAI,CAAC,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,CAChC,CAAC;IACF,MAAM,QAAQ,GAAG,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAClD,oBAAoB,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IACrE,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,SAAS,oBAAoB,CAC3B,OAAe,EACf,UAAkB,EAClB,OAAe;IAEf,IAAI,UAAU,GAAG,kBAAkB,CAAC,UAAU,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CACb,GAAG,OAAO,uBAAuB,kBAAkB,CAAC,UAAU,EAAE,CACjE,CAAC;IACJ,CAAC;IACD,MAAM,eAAe,GAAG,sBAAsB,CAAC,OAAO,CAAC,CAAC;IACxD,IAAI,eAAe,GAAG,kBAAkB,CAAC,SAAS,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CACb,GAAG,OAAO,sBAAsB,kBAAkB,CAAC,SAAS,cAAc,eAAe,GAAG,CAC7F,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,cAAc,CAAC,OAAe;IACrC,IAAI,OAAO,KAAK,EAAE,EAAE,CAAC;QACnB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,IAAI,CAAC;QACH,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC,CAAC;IACtE,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from "./learnings-contract.js";
|
|
2
|
+
export * from "./learnings-writer.js";
|
|
3
|
+
export { DEFAULT_PROJECT_RULES_FILE, PROJECT_LEARNINGS_FILENAME, resolveProjectLearningsFile, resolveProjectRulesFile, type ProjectConfig, } from "./project-config.js";
|
|
4
|
+
//# sourceMappingURL=learnings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"learnings.d.ts","sourceRoot":"","sources":["../../src/core/learnings.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,2BAA2B,EAC3B,uBAAuB,EACvB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"learnings.js","sourceRoot":"","sources":["../../src/core/learnings.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,2BAA2B,EAC3B,uBAAuB,GAExB,MAAM,qBAAqB,CAAC"}
|
package/dist/core/lisa.d.ts
CHANGED
|
@@ -76,6 +76,10 @@ export declare class Lisa {
|
|
|
76
76
|
* skips existing files, so without help the parent would win).
|
|
77
77
|
*/
|
|
78
78
|
private copyOverwriteOwnership;
|
|
79
|
+
/** Destination selected by the host project's projectRulesFile setting. */
|
|
80
|
+
private projectLearningsFile;
|
|
81
|
+
/** Whether the canonical all/create-only learnings source is registered. */
|
|
82
|
+
private projectLearningsTemplateRegistered;
|
|
79
83
|
private readonly separator;
|
|
80
84
|
private readonly lisaignoreSuffix;
|
|
81
85
|
/**
|
|
@@ -137,6 +141,12 @@ export declare class Lisa {
|
|
|
137
141
|
* @returns Promise that resolves once pending deletions have been collected
|
|
138
142
|
*/
|
|
139
143
|
private loadPendingDeletions;
|
|
144
|
+
/**
|
|
145
|
+
* Resolve the host-owned learnings destination before template ownership is
|
|
146
|
+
* computed. The canonical source stays at its stable package path while its
|
|
147
|
+
* destination follows the configured project-rules directory.
|
|
148
|
+
*/
|
|
149
|
+
private loadProjectLearningsFile;
|
|
140
150
|
/**
|
|
141
151
|
* Pre-compute, for every relative path shipped by any type's `create-only/`
|
|
142
152
|
* directory, which single type "owns" it. The owner is the most-specific
|
|
@@ -398,6 +408,22 @@ export declare class Lisa {
|
|
|
398
408
|
* @param currentType Project type (e.g. "all", "typescript", "cdk") supplying these files
|
|
399
409
|
*/
|
|
400
410
|
private processDirectory;
|
|
411
|
+
/**
|
|
412
|
+
* Map the stable canonical learnings source to the project's configured
|
|
413
|
+
* sibling path. No other template source is redirected.
|
|
414
|
+
* @param sourceRelativePath - Path relative to the strategy source directory
|
|
415
|
+
* @param strategyName - Strategy delivering the source file
|
|
416
|
+
* @param currentType - Project type that owns the source directory
|
|
417
|
+
* @returns Destination path relative to the host project
|
|
418
|
+
*/
|
|
419
|
+
private resolveTemplateDestination;
|
|
420
|
+
/**
|
|
421
|
+
* Identify the single machine-managed file that remains host-owned after
|
|
422
|
+
* creation, regardless of later stack templates or deletion manifests.
|
|
423
|
+
* @param relativePath - Candidate destination path
|
|
424
|
+
* @returns Whether the path is the registered project learnings destination
|
|
425
|
+
*/
|
|
426
|
+
private isProjectLearningsPath;
|
|
401
427
|
/**
|
|
402
428
|
* Decide whether a candidate source file should be passed to its strategy.
|
|
403
429
|
* Centralizes three independent skip rules that all need to short-circuit
|
package/dist/core/lisa.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lisa.d.ts","sourceRoot":"","sources":["../../src/core/lisa.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAyBnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAOzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EACL,iBAAiB,EAGlB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAwB,MAAM,wBAAwB,CAAC;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAc9D,OAAO,KAAK,EAIV,UAAU,EACV,UAAU,EAGX,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"lisa.d.ts","sourceRoot":"","sources":["../../src/core/lisa.ts"],"names":[],"mappings":"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"}
|
package/dist/core/lisa.js
CHANGED
|
@@ -25,11 +25,15 @@ import { loadIgnorePatterns, } from "../utils/ignore-patterns.js";
|
|
|
25
25
|
import { getLisaDistDir, hashFile, isRunningAsTrampoline, regenerateLockfilesInProcess, scheduleReconciliationChild, shouldSchedulePostinstallReconciliation, } from "../utils/postinstall-trampoline.js";
|
|
26
26
|
import { COPY_STRATEGIES, createInitialCounters, harnessIncludesAgent, } from "./config.js";
|
|
27
27
|
import { migrateInstructionFiles } from "./instruction-files-migration.js";
|
|
28
|
+
import { assertSafeLearningParents, resolveSafeLearningTarget, } from "./learnings-file-safety.js";
|
|
29
|
+
import { readProjectConfig, resolveProjectLearningsFile, } from "./project-config.js";
|
|
28
30
|
/**
|
|
29
31
|
* Log fragment used when a create-only instruction file already exists and is
|
|
30
32
|
* therefore left untouched (the host owns it).
|
|
31
33
|
*/
|
|
32
34
|
const HOST_OWNED_LABEL = "already present (host-owned)";
|
|
35
|
+
const CREATE_ONLY_STRATEGY = "create-only";
|
|
36
|
+
const PROJECT_LEARNINGS_TEMPLATE_PATH = path.join(".claude", "rules", "PROJECT_LEARNINGS.md");
|
|
33
37
|
/**
|
|
34
38
|
* Main Lisa orchestrator
|
|
35
39
|
*/
|
|
@@ -91,6 +95,10 @@ export class Lisa {
|
|
|
91
95
|
* skips existing files, so without help the parent would win).
|
|
92
96
|
*/
|
|
93
97
|
copyOverwriteOwnership = new Map();
|
|
98
|
+
/** Destination selected by the host project's projectRulesFile setting. */
|
|
99
|
+
projectLearningsFile = PROJECT_LEARNINGS_TEMPLATE_PATH;
|
|
100
|
+
/** Whether the canonical all/create-only learnings source is registered. */
|
|
101
|
+
projectLearningsTemplateRegistered = false;
|
|
94
102
|
separator = "========================================";
|
|
95
103
|
lisaignoreSuffix = "(.lisaignore)";
|
|
96
104
|
/**
|
|
@@ -199,11 +207,22 @@ export class Lisa {
|
|
|
199
207
|
for (const type of ["all", ...this.detectedTypes]) {
|
|
200
208
|
const paths = await this.readPendingDeletionsForType(type);
|
|
201
209
|
for (const p of paths) {
|
|
202
|
-
pending.add(p);
|
|
210
|
+
pending.add(path.normalize(p));
|
|
203
211
|
}
|
|
204
212
|
}
|
|
205
213
|
this.pendingDeletions = pending;
|
|
206
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Resolve the host-owned learnings destination before template ownership is
|
|
217
|
+
* computed. The canonical source stays at its stable package path while its
|
|
218
|
+
* destination follows the configured project-rules directory.
|
|
219
|
+
*/
|
|
220
|
+
async loadProjectLearningsFile() {
|
|
221
|
+
const relativeFile = resolveProjectLearningsFile(await readProjectConfig(this.config.destDir));
|
|
222
|
+
const { root, target } = resolveSafeLearningTarget(this.config.destDir, relativeFile);
|
|
223
|
+
await assertSafeLearningParents(root, path.dirname(target));
|
|
224
|
+
this.projectLearningsFile = path.normalize(relativeFile);
|
|
225
|
+
}
|
|
207
226
|
/**
|
|
208
227
|
* Pre-compute, for every relative path shipped by any type's `create-only/`
|
|
209
228
|
* directory, which single type "owns" it. The owner is the most-specific
|
|
@@ -230,17 +249,25 @@ export class Lisa {
|
|
|
230
249
|
async loadCreateOnlyOwnership() {
|
|
231
250
|
const ownership = new Map();
|
|
232
251
|
for (const type of ["all", ...this.detectedTypes]) {
|
|
233
|
-
const createOnlyDir = path.join(this.config.lisaDir, type,
|
|
252
|
+
const createOnlyDir = path.join(this.config.lisaDir, type, CREATE_ONLY_STRATEGY);
|
|
234
253
|
if (!(await fse.pathExists(createOnlyDir))) {
|
|
235
254
|
continue;
|
|
236
255
|
}
|
|
237
256
|
const files = await listFilesRecursive(createOnlyDir);
|
|
238
257
|
for (const file of files) {
|
|
239
|
-
const
|
|
258
|
+
const sourceRelativePath = path.relative(createOnlyDir, file);
|
|
259
|
+
const relativePath = this.resolveTemplateDestination(sourceRelativePath, CREATE_ONLY_STRATEGY, type);
|
|
240
260
|
// Later types overwrite earlier ones, so child wins over parent.
|
|
241
261
|
ownership.set(relativePath, type);
|
|
242
262
|
}
|
|
243
263
|
}
|
|
264
|
+
const canonicalSource = path.join(this.config.lisaDir, "all", CREATE_ONLY_STRATEGY, PROJECT_LEARNINGS_TEMPLATE_PATH);
|
|
265
|
+
this.projectLearningsTemplateRegistered =
|
|
266
|
+
await fse.pathExists(canonicalSource);
|
|
267
|
+
if (this.projectLearningsTemplateRegistered) {
|
|
268
|
+
// Durable project memory is a single contract, not a stack override.
|
|
269
|
+
ownership.set(this.projectLearningsFile, "all");
|
|
270
|
+
}
|
|
244
271
|
this.createOnlyOwnership = ownership;
|
|
245
272
|
}
|
|
246
273
|
/**
|
|
@@ -345,6 +372,13 @@ export class Lisa {
|
|
|
345
372
|
*/
|
|
346
373
|
async processSingleDeletion(relativePath) {
|
|
347
374
|
const { logger } = this.deps;
|
|
375
|
+
const normalizedRelativePath = path.normalize(relativePath);
|
|
376
|
+
if (this.projectLearningsTemplateRegistered &&
|
|
377
|
+
normalizedRelativePath === this.projectLearningsFile) {
|
|
378
|
+
logger.info(`Kept (host-owned project learnings): ${relativePath}`);
|
|
379
|
+
this.counters.skipped++;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
348
382
|
const targetPath = path.join(this.config.destDir, relativePath);
|
|
349
383
|
const resolvedTarget = path.resolve(targetPath);
|
|
350
384
|
const resolvedDest = path.resolve(this.config.destDir);
|
|
@@ -564,6 +598,7 @@ export class Lisa {
|
|
|
564
598
|
await this.initServices();
|
|
565
599
|
await this.detectTypes();
|
|
566
600
|
await this.detectSelfApply();
|
|
601
|
+
await this.loadProjectLearningsFile();
|
|
567
602
|
await this.loadPendingDeletions();
|
|
568
603
|
await this.loadCreateOnlyOwnership();
|
|
569
604
|
await this.loadCopyOverwriteOwnership();
|
|
@@ -1003,7 +1038,15 @@ export class Lisa {
|
|
|
1003
1038
|
*/
|
|
1004
1039
|
async processDirectory(srcDir, strategy, currentType) {
|
|
1005
1040
|
const allFiles = await listFilesRecursive(srcDir);
|
|
1006
|
-
const files = allFiles
|
|
1041
|
+
const files = allFiles
|
|
1042
|
+
.map(srcFile => {
|
|
1043
|
+
const sourceRelativePath = path.relative(srcDir, srcFile);
|
|
1044
|
+
return {
|
|
1045
|
+
srcFile,
|
|
1046
|
+
relativePath: this.resolveTemplateDestination(sourceRelativePath, strategy.name, currentType),
|
|
1047
|
+
};
|
|
1048
|
+
})
|
|
1049
|
+
.filter(({ relativePath }) => this.shouldProcessFile(relativePath, strategy.name, currentType));
|
|
1007
1050
|
const context = {
|
|
1008
1051
|
config: this.config,
|
|
1009
1052
|
backupFile: async (absolutePath) => {
|
|
@@ -1014,14 +1057,39 @@ export class Lisa {
|
|
|
1014
1057
|
return this.handleOverwritePrompt(relativePath, sourcePath, destPath);
|
|
1015
1058
|
},
|
|
1016
1059
|
};
|
|
1017
|
-
for (const srcFile of files) {
|
|
1018
|
-
const relativePath = path.relative(srcDir, srcFile);
|
|
1060
|
+
for (const { srcFile, relativePath } of files) {
|
|
1019
1061
|
const destFile = path.join(this.config.destDir, relativePath);
|
|
1020
1062
|
const result = await strategy.apply(srcFile, destFile, relativePath, context);
|
|
1021
1063
|
this.updateCounters(result);
|
|
1022
1064
|
this.logResult(result);
|
|
1023
1065
|
}
|
|
1024
1066
|
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Map the stable canonical learnings source to the project's configured
|
|
1069
|
+
* sibling path. No other template source is redirected.
|
|
1070
|
+
* @param sourceRelativePath - Path relative to the strategy source directory
|
|
1071
|
+
* @param strategyName - Strategy delivering the source file
|
|
1072
|
+
* @param currentType - Project type that owns the source directory
|
|
1073
|
+
* @returns Destination path relative to the host project
|
|
1074
|
+
*/
|
|
1075
|
+
resolveTemplateDestination(sourceRelativePath, strategyName, currentType) {
|
|
1076
|
+
if (currentType === "all" &&
|
|
1077
|
+
strategyName === CREATE_ONLY_STRATEGY &&
|
|
1078
|
+
sourceRelativePath === PROJECT_LEARNINGS_TEMPLATE_PATH) {
|
|
1079
|
+
return this.projectLearningsFile;
|
|
1080
|
+
}
|
|
1081
|
+
return sourceRelativePath;
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Identify the single machine-managed file that remains host-owned after
|
|
1085
|
+
* creation, regardless of later stack templates or deletion manifests.
|
|
1086
|
+
* @param relativePath - Candidate destination path
|
|
1087
|
+
* @returns Whether the path is the registered project learnings destination
|
|
1088
|
+
*/
|
|
1089
|
+
isProjectLearningsPath(relativePath) {
|
|
1090
|
+
return (this.projectLearningsTemplateRegistered &&
|
|
1091
|
+
relativePath === this.projectLearningsFile);
|
|
1092
|
+
}
|
|
1025
1093
|
/**
|
|
1026
1094
|
* Decide whether a candidate source file should be passed to its strategy.
|
|
1027
1095
|
* Centralizes three independent skip rules that all need to short-circuit
|
|
@@ -1049,12 +1117,18 @@ export class Lisa {
|
|
|
1049
1117
|
this.logSkip(`ignored`, relativePath, "Ignored");
|
|
1050
1118
|
return false;
|
|
1051
1119
|
}
|
|
1052
|
-
|
|
1120
|
+
const isProjectLearnings = this.isProjectLearningsPath(relativePath);
|
|
1121
|
+
if (this.pendingDeletions.has(relativePath) && !isProjectLearnings) {
|
|
1053
1122
|
this.counters.skipped++;
|
|
1054
1123
|
this.logSkip("pending deletion by detected type", relativePath, "Skipped (pending deletion)");
|
|
1055
1124
|
return false;
|
|
1056
1125
|
}
|
|
1057
|
-
if (strategyName
|
|
1126
|
+
if (isProjectLearnings && strategyName !== CREATE_ONLY_STRATEGY) {
|
|
1127
|
+
this.counters.skipped++;
|
|
1128
|
+
this.logSkip("owned by all/create-only", relativePath, "Skipped (host-owned project learnings)");
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
if (strategyName === CREATE_ONLY_STRATEGY) {
|
|
1058
1132
|
const owner = this.createOnlyOwnership.get(relativePath);
|
|
1059
1133
|
if (owner !== undefined && owner !== currentType) {
|
|
1060
1134
|
this.counters.skipped++;
|