forgeos 0.1.0-alpha.24 → 0.1.0-alpha.26
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/AGENTS.md +1 -1
- package/CHANGELOG.md +28 -0
- package/adapters/java/target/forge-java-adapter-0.1.0-alpha.11.jar +0 -0
- package/adapters/java-spring-boot-starter/target/forge-java-spring-boot-starter-0.1.0-alpha.11.jar +0 -0
- package/docs/changelog.md +22 -0
- package/examples/java-billing/target/java-billing-0.1.0-alpha.11-all.jar +0 -0
- package/examples/java-billing/target/java-billing-0.1.0-alpha.11.jar +0 -0
- package/package.json +1 -1
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-memory/bridge.ts +67 -7
- package/src/forge/agent-memory/types.ts +4 -0
- package/src/forge/cli/changed.ts +7 -6
- package/src/forge/cli/handoff.ts +5 -2
- package/src/forge/compiler/agent-contract/build.ts +91 -10
- package/src/forge/delta/recorder.ts +136 -58
- package/src/forge/delta/store.ts +36 -3
- package/src/forge/make/index.ts +11 -2
- package/src/forge/version.ts +1 -1
- package/src/forge/workspace/git-summary.ts +57 -8
package/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.26 input=23c00c5407aab4088b55b49905f2b195667a75b8544fa180786ac637ee67e1e5 content=0d493cf0e41b71cb652d5e0e1b0c1f83d2a1281b748321f0b00f0773ba93074e
|
|
2
2
|
# AGENTS.md
|
|
3
3
|
|
|
4
4
|
<!-- forge-generated:start -->
|
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
# forgeos
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.26
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Harden the field-demo loop after the Team Onboarding app exercise.
|
|
8
|
+
|
|
9
|
+
- Let `forge changed` and `forge handoff` summarize non-git workspaces with a filesystem inventory instead of reporting zero useful changes.
|
|
10
|
+
- Keep `forge make resource` global by default unless a tenants table exists or `--tenant-scoped` is explicit.
|
|
11
|
+
- Expand capability-map table detection for aliased `ctx.db` usage.
|
|
12
|
+
- Wait through short-lived DeltaDB writer locks before reporting `FORGE_DELTA_BUSY`.
|
|
13
|
+
|
|
3
14
|
## Unreleased
|
|
4
15
|
|
|
16
|
+
## 0.1.0-alpha.25
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Harden DeltaDB and Agent Memory under real `forge dev` concurrency.
|
|
21
|
+
|
|
22
|
+
- Stop long-running dev recorders from holding the DeltaDB writer lock between events.
|
|
23
|
+
- Retry short transient DeltaDB writer conflicts before reporting `FORGE_DELTA_BUSY`.
|
|
24
|
+
- Keep Codex hook queue checkpoints unchanged when Agent Memory ingest is blocked by a busy DeltaDB writer, then retry safely instead of losing queued events.
|
|
25
|
+
- Add watcher backoff metadata for lock recovery and document the safe queue/DeltaDB behavior.
|
|
26
|
+
|
|
27
|
+
- Fix tenant-scope reporting in the generated agent contract and capability map.
|
|
28
|
+
|
|
29
|
+
- Match tenant-scoped tables by both authored/camelCase table names and generated SQL snake_case table names.
|
|
30
|
+
- Report camelCase liveQuery dependencies such as `onboardingTasks` as `tenant` scoped when `tenantScope.json` confirms `tenant_id`.
|
|
31
|
+
- Add regression coverage for the Team Onboarding style liveQuery/capability-map path.
|
|
32
|
+
|
|
5
33
|
## 0.1.0-alpha.24
|
|
6
34
|
|
|
7
35
|
### Patch Changes
|
|
Binary file
|
package/adapters/java-spring-boot-starter/target/forge-java-spring-boot-starter-0.1.0-alpha.11.jar
CHANGED
|
Binary file
|
package/docs/changelog.md
CHANGED
|
@@ -6,6 +6,28 @@ The canonical source file in the repository is `CHANGELOG.md`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.1.0-alpha.26
|
|
10
|
+
|
|
11
|
+
- Hardened the field-demo loop after the Team Onboarding app exercise:
|
|
12
|
+
non-git workspaces now get a filesystem-backed `changed`/`handoff` summary,
|
|
13
|
+
`forge make resource` stays global unless tenant scope is explicit or already
|
|
14
|
+
modeled, capability-map extraction sees aliased `ctx.db` table usage, and
|
|
15
|
+
Agent Memory waits through short-lived DeltaDB writer locks before reporting
|
|
16
|
+
`FORGE_DELTA_BUSY`.
|
|
17
|
+
|
|
18
|
+
## 0.1.0-alpha.25
|
|
19
|
+
|
|
20
|
+
- Hardened DeltaDB and Agent Memory for concurrent `forge dev` usage:
|
|
21
|
+
dev recorders now release the writer lock between events, Agent Memory ingest
|
|
22
|
+
retries short transient writer conflicts, and queued Codex hook events keep
|
|
23
|
+
their checkpoint unchanged when DeltaDB is temporarily busy.
|
|
24
|
+
- Fixed tenant-scope reporting in the generated agent contract and capability
|
|
25
|
+
map for camelCase authored tables such as `onboardingTasks`, so liveQuery
|
|
26
|
+
dependencies now report `tenant` scope when `tenantScope.json` confirms the
|
|
27
|
+
table is tenant-scoped.
|
|
28
|
+
- Added regression tests and docs for the DeltaDB lock recovery path and the
|
|
29
|
+
Team Onboarding style capability-map tenant-scope path.
|
|
30
|
+
|
|
9
31
|
## 0.1.0-alpha.24
|
|
10
32
|
|
|
11
33
|
- Consolidated the public alpha adoption surface: MIT license, package license
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.
|
|
1
|
+
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.26","releaseId":"forgeos@0.1.0-alpha.26+unknown","schemaVersion":"0.1.0"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.26 input=23c00c5407aab4088b55b49905f2b195667a75b8544fa180786ac637ee67e1e5 content=967d4e05aa540b3e3d843fa6ed4516658ab4c364c1d8c42a14f36c57b75dff71
|
|
2
2
|
export const releaseManifest = {
|
|
3
3
|
"defaultProvider": "local",
|
|
4
4
|
"diagnostics": [],
|
|
@@ -19,7 +19,7 @@ export const releaseManifest = {
|
|
|
19
19
|
"custom"
|
|
20
20
|
],
|
|
21
21
|
"packageName": "forgeos",
|
|
22
|
-
"packageVersion": "0.1.0-alpha.
|
|
23
|
-
"releaseId": "forgeos@0.1.0-alpha.
|
|
22
|
+
"packageVersion": "0.1.0-alpha.26",
|
|
23
|
+
"releaseId": "forgeos@0.1.0-alpha.26+unknown",
|
|
24
24
|
"schemaVersion": "0.1.0"
|
|
25
25
|
} as const;
|
|
@@ -113,10 +113,19 @@ async function openMemoryStore(
|
|
|
113
113
|
workspaceRoot: string,
|
|
114
114
|
access: "read" | "write" = "write",
|
|
115
115
|
): Promise<DeltaStore | AgentMemoryUnavailableResult> {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
const retryDelays = access === "write" ? [25, 75, 150] : [];
|
|
117
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
118
|
+
try {
|
|
119
|
+
return await DeltaStore.open(workspaceRoot, {
|
|
120
|
+
access,
|
|
121
|
+
...(access === "write" ? { waitMs: 1_500, retryDelayMs: 50 } : {}),
|
|
122
|
+
});
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (!(error instanceof DeltaStoreBusyError) || attempt >= retryDelays.length) {
|
|
125
|
+
return memoryUnavailable(error, workspaceRoot);
|
|
126
|
+
}
|
|
127
|
+
await sleep(retryDelays[attempt] ?? 0);
|
|
128
|
+
}
|
|
120
129
|
}
|
|
121
130
|
}
|
|
122
131
|
|
|
@@ -131,6 +140,10 @@ function isExternalPgliteRead(result: AgentMemoryUnavailableResult): boolean {
|
|
|
131
140
|
);
|
|
132
141
|
}
|
|
133
142
|
|
|
143
|
+
function isDeltaBusyIngestResult(result: AgentIngestResult): boolean {
|
|
144
|
+
return result.ok === false && result.busy?.code === "FORGE_DELTA_BUSY";
|
|
145
|
+
}
|
|
146
|
+
|
|
134
147
|
function fallbackMemoryPath(workspaceRoot: string): string {
|
|
135
148
|
return join(workspaceRoot, ".forge", "agent", "events.ndjson");
|
|
136
149
|
}
|
|
@@ -413,23 +426,29 @@ async function ingestAgentMemoryQueueFile(options: AgentMemoryCommandOptions): P
|
|
|
413
426
|
source,
|
|
414
427
|
eventName: options.eventName,
|
|
415
428
|
});
|
|
429
|
+
const errors = [
|
|
430
|
+
...drained.errors,
|
|
431
|
+
...(drained.busy ? ["DeltaDB is busy; queue checkpoint was not advanced"] : []),
|
|
432
|
+
];
|
|
416
433
|
return {
|
|
417
|
-
ok:
|
|
434
|
+
ok: errors.length === 0,
|
|
418
435
|
watch: false,
|
|
419
436
|
source,
|
|
420
437
|
file: options.file,
|
|
421
438
|
eventsIngested: drained.eventsIngested,
|
|
422
|
-
errors
|
|
439
|
+
errors,
|
|
423
440
|
bytesRead: drained.bytesRead,
|
|
424
441
|
pendingBytes: drained.pendingBytes,
|
|
425
442
|
checkpointFile: drained.checkpointFile,
|
|
426
443
|
compacted: drained.compacted,
|
|
427
444
|
historyFile: drained.historyFile,
|
|
445
|
+
...(drained.busy ? { busy: drained.busy, pendingDueToBusy: true } : {}),
|
|
428
446
|
nextActions: [
|
|
447
|
+
...(drained.busy ? ["forge delta status --json"] : []),
|
|
429
448
|
`forge agent memory --entry ${source} --json`,
|
|
430
449
|
`forge agent hooks status --target ${source} --json`,
|
|
431
450
|
],
|
|
432
|
-
exitCode:
|
|
451
|
+
exitCode: errors.length === 0 ? 0 : 1,
|
|
433
452
|
};
|
|
434
453
|
}
|
|
435
454
|
|
|
@@ -644,6 +663,7 @@ export async function drainAgentMemoryQueueFile(options: {
|
|
|
644
663
|
checkpointFile: string;
|
|
645
664
|
compacted: boolean;
|
|
646
665
|
historyFile: string;
|
|
666
|
+
busy?: AgentMemoryUnavailableResult["busy"];
|
|
647
667
|
}> {
|
|
648
668
|
const historyFile = queueHistoryPath(options.watchFile);
|
|
649
669
|
if (!existsSync(options.watchFile)) {
|
|
@@ -699,6 +719,17 @@ export async function drainAgentMemoryQueueFile(options: {
|
|
|
699
719
|
eventsIngested += 1;
|
|
700
720
|
consumedOffset = bytesRead + line.endOffset;
|
|
701
721
|
writeQueueCheckpoint(options.watchFile, consumedOffset);
|
|
722
|
+
} else if (isDeltaBusyIngestResult(result)) {
|
|
723
|
+
return {
|
|
724
|
+
eventsIngested,
|
|
725
|
+
errors,
|
|
726
|
+
bytesRead,
|
|
727
|
+
pendingBytes,
|
|
728
|
+
checkpointFile: queueCheckpointPath(options.watchFile),
|
|
729
|
+
compacted: false,
|
|
730
|
+
historyFile,
|
|
731
|
+
busy: result.busy,
|
|
732
|
+
};
|
|
702
733
|
} else {
|
|
703
734
|
errors.push(result.error ?? "agent memory ingest failed");
|
|
704
735
|
break;
|
|
@@ -886,7 +917,19 @@ async function watchAgentMemoryIngest(options: AgentMemoryCommandOptions): Promi
|
|
|
886
917
|
|
|
887
918
|
let eventsIngested = 0;
|
|
888
919
|
const errors: string[] = [];
|
|
920
|
+
let busyRetries = 0;
|
|
921
|
+
let lastBusy: AgentMemoryUnavailableResult["busy"] | undefined;
|
|
889
922
|
let pendingIngest = Promise.resolve();
|
|
923
|
+
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
924
|
+
const scheduleBusyRetry = () => {
|
|
925
|
+
if (retryTimer) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
retryTimer = setTimeout(() => {
|
|
929
|
+
retryTimer = undefined;
|
|
930
|
+
pendingIngest = pendingIngest.then(ingestNewContent, ingestNewContent);
|
|
931
|
+
}, 500);
|
|
932
|
+
};
|
|
890
933
|
const ingestNewContent = async () => {
|
|
891
934
|
const result = await drainAgentMemoryQueueFile({
|
|
892
935
|
workspaceRoot: options.workspaceRoot,
|
|
@@ -895,6 +938,13 @@ async function watchAgentMemoryIngest(options: AgentMemoryCommandOptions): Promi
|
|
|
895
938
|
eventName: options.eventName,
|
|
896
939
|
});
|
|
897
940
|
eventsIngested += result.eventsIngested;
|
|
941
|
+
if (result.busy) {
|
|
942
|
+
busyRetries += 1;
|
|
943
|
+
lastBusy = result.busy;
|
|
944
|
+
scheduleBusyRetry();
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
lastBusy = undefined;
|
|
898
948
|
errors.push(...result.errors);
|
|
899
949
|
};
|
|
900
950
|
|
|
@@ -904,6 +954,10 @@ async function watchAgentMemoryIngest(options: AgentMemoryCommandOptions): Promi
|
|
|
904
954
|
pendingIngest = pendingIngest.then(ingestNewContent, ingestNewContent);
|
|
905
955
|
});
|
|
906
956
|
const shutdown = () => {
|
|
957
|
+
if (retryTimer) {
|
|
958
|
+
clearTimeout(retryTimer);
|
|
959
|
+
retryTimer = undefined;
|
|
960
|
+
}
|
|
907
961
|
watcher.close();
|
|
908
962
|
void pendingIngest.finally(() => {
|
|
909
963
|
resolve({
|
|
@@ -913,7 +967,9 @@ async function watchAgentMemoryIngest(options: AgentMemoryCommandOptions): Promi
|
|
|
913
967
|
file,
|
|
914
968
|
eventsIngested,
|
|
915
969
|
errors,
|
|
970
|
+
...(lastBusy ? { busy: lastBusy, pendingDueToBusy: true, busyRetries } : {}),
|
|
916
971
|
nextActions: [
|
|
972
|
+
...(lastBusy ? ["forge delta status --json"] : []),
|
|
917
973
|
`forge agent memory --entry ${source} --json`,
|
|
918
974
|
`forge agent hooks status --target ${source} --json`,
|
|
919
975
|
],
|
|
@@ -971,6 +1027,10 @@ function installAgentMemory(options: AgentMemoryCommandOptions): AgentInstallRes
|
|
|
971
1027
|
return cursorInstallResult(filesWritten, planned);
|
|
972
1028
|
}
|
|
973
1029
|
|
|
1030
|
+
function sleep(ms: number): Promise<void> {
|
|
1031
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1032
|
+
}
|
|
1033
|
+
|
|
974
1034
|
export function formatAgentMemoryJson(result: AgentMemoryCommandResult): string {
|
|
975
1035
|
return `${JSON.stringify(result, null, 2)}\n`;
|
|
976
1036
|
}
|
|
@@ -155,6 +155,7 @@ export interface AgentIngestResult {
|
|
|
155
155
|
path: string;
|
|
156
156
|
reason: "pglite-active";
|
|
157
157
|
};
|
|
158
|
+
busy?: AgentMemoryUnavailableResult["busy"];
|
|
158
159
|
exitCode: 0 | 1;
|
|
159
160
|
error?: string;
|
|
160
161
|
diagnostics?: Diagnostic[];
|
|
@@ -174,6 +175,9 @@ export interface AgentIngestWatchResult {
|
|
|
174
175
|
checkpointFile?: string;
|
|
175
176
|
compacted?: boolean;
|
|
176
177
|
historyFile?: string;
|
|
178
|
+
busy?: AgentMemoryUnavailableResult["busy"];
|
|
179
|
+
pendingDueToBusy?: boolean;
|
|
180
|
+
busyRetries?: number;
|
|
177
181
|
nextActions: string[];
|
|
178
182
|
exitCode: 0 | 1;
|
|
179
183
|
}
|
package/src/forge/cli/changed.ts
CHANGED
|
@@ -82,8 +82,7 @@ function buildRisks(git: WorkspaceGitSummary): string[] {
|
|
|
82
82
|
const risks: string[] = [];
|
|
83
83
|
const changed = git.changeSummary.changed;
|
|
84
84
|
if (!git.available) {
|
|
85
|
-
risks.push("git status is unavailable;
|
|
86
|
-
return risks;
|
|
85
|
+
risks.push("git status is unavailable; using filesystem inventory as untracked-file analysis");
|
|
87
86
|
}
|
|
88
87
|
if (git.untracked.count > 0) {
|
|
89
88
|
risks.push(`${git.untracked.count} untracked file(s) are not in git history`);
|
|
@@ -103,7 +102,7 @@ function buildRisks(git: WorkspaceGitSummary): string[] {
|
|
|
103
102
|
|
|
104
103
|
function buildRecommendedCommands(git: WorkspaceGitSummary): string[] {
|
|
105
104
|
if (!git.available) {
|
|
106
|
-
return ["
|
|
105
|
+
return ["forge status --json", "forge handoff --json", "git init"];
|
|
107
106
|
}
|
|
108
107
|
if (git.changeSummary.changed.total.count === 0) {
|
|
109
108
|
return ["forge status --json", "forge dev --once --json"];
|
|
@@ -202,12 +201,13 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
|
|
|
202
201
|
const reviewFocus = buildReviewFocus(humanChanges, viewDerivedChanges);
|
|
203
202
|
const generatedExplanation = buildGeneratedChangeExplanation(humanChanges, viewDerivedChanges);
|
|
204
203
|
const diffPlan: DiffPlan = buildDiffPlanFromChangeSummary(viewChanged);
|
|
204
|
+
const ok = git.available || git.source === "filesystem";
|
|
205
205
|
|
|
206
206
|
return {
|
|
207
|
-
ok
|
|
207
|
+
ok,
|
|
208
208
|
data: {
|
|
209
209
|
schemaVersion: "0.1.0",
|
|
210
|
-
ok
|
|
210
|
+
ok,
|
|
211
211
|
summary: {
|
|
212
212
|
branch: git.branch,
|
|
213
213
|
commit: git.commit,
|
|
@@ -223,6 +223,7 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
|
|
|
223
223
|
},
|
|
224
224
|
git: {
|
|
225
225
|
available: git.available,
|
|
226
|
+
source: git.source,
|
|
226
227
|
...(git.error ? { error: git.error } : {}),
|
|
227
228
|
branch: git.branch,
|
|
228
229
|
commit: git.commit,
|
|
@@ -240,7 +241,7 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
|
|
|
240
241
|
recommendedCommands,
|
|
241
242
|
nextActions: recommendedCommands,
|
|
242
243
|
},
|
|
243
|
-
exitCode:
|
|
244
|
+
exitCode: ok ? 0 : 1,
|
|
244
245
|
};
|
|
245
246
|
}
|
|
246
247
|
|
package/src/forge/cli/handoff.ts
CHANGED
|
@@ -140,6 +140,7 @@ function buildOpeningBrief(input: {
|
|
|
140
140
|
}): string {
|
|
141
141
|
const agent = input.dev.summary.agentContext;
|
|
142
142
|
const changedByType = summarizeChangeTypes(input.git.changeSummary.changed);
|
|
143
|
+
const changedFiles = Math.max(agent.changedFiles, input.git.changed.count);
|
|
143
144
|
const tests = input.recentRuns.test
|
|
144
145
|
? input.recentRuns.test.ok
|
|
145
146
|
? "last test run passed"
|
|
@@ -150,7 +151,7 @@ function buildOpeningBrief(input: {
|
|
|
150
151
|
: "no blocking issues";
|
|
151
152
|
return [
|
|
152
153
|
`ForgeOS handoff: ${input.dev.ok ? "dev diagnostics are clean" : "dev diagnostics need attention"}.`,
|
|
153
|
-
`${
|
|
154
|
+
`${changedFiles} changed file(s)${changedByType ? `: ${changedByType}` : ""}; ${input.git.staged.count} staged, ${input.git.untracked.count} untracked.`,
|
|
154
155
|
`${tests}; ${blockers}.`,
|
|
155
156
|
`Next command: ${input.dev.summary.primaryAction?.command ?? input.dev.nextActions[0]?.command ?? "forge dev"}.`,
|
|
156
157
|
].join(" ");
|
|
@@ -168,6 +169,7 @@ export async function runHandoffCommand(options: HandoffCommandOptions): Promise
|
|
|
168
169
|
const agent = dev.summary.agentContext;
|
|
169
170
|
const risks = [
|
|
170
171
|
...agent.blockingIssues,
|
|
172
|
+
...(!git.available ? ["git status is unavailable; using filesystem inventory as untracked-file analysis"] : []),
|
|
171
173
|
...(git.untracked.count > 0 ? [`${git.untracked.count} untracked file(s) are not in git history`] : []),
|
|
172
174
|
...(recentRuns.test && !recentRuns.test.ok ? ["last test run failed"] : []),
|
|
173
175
|
...(recentRuns.ui && !recentRuns.ui.ok ? ["last UI run failed"] : []),
|
|
@@ -181,6 +183,7 @@ export async function runHandoffCommand(options: HandoffCommandOptions): Promise
|
|
|
181
183
|
agent.blockingIssues.length === 0 &&
|
|
182
184
|
(!recentRuns.test || recentRuns.test.ok) &&
|
|
183
185
|
(!recentRuns.ui || recentRuns.ui.ok);
|
|
186
|
+
const changedFiles = Math.max(agent.changedFiles, git.changed.count);
|
|
184
187
|
|
|
185
188
|
return {
|
|
186
189
|
schemaVersion: "0.1.0",
|
|
@@ -192,7 +195,7 @@ export async function runHandoffCommand(options: HandoffCommandOptions): Promise
|
|
|
192
195
|
generatedChanged: agent.generatedChanged,
|
|
193
196
|
generatedChangedFiles: agent.generatedChangedFiles,
|
|
194
197
|
frontendReady: agent.frontendReady,
|
|
195
|
-
changedFiles
|
|
198
|
+
changedFiles,
|
|
196
199
|
stagedFiles: git.staged.count,
|
|
197
200
|
unstagedFiles: git.unstaged.count,
|
|
198
201
|
untrackedFiles: git.untracked.count,
|
|
@@ -7,6 +7,7 @@ import { detectSecrets } from "../classifier/secrets.ts";
|
|
|
7
7
|
import { GENERATOR_VERSION } from "../emitter/constants.ts";
|
|
8
8
|
import { stripDeterministicHeader } from "../primitives/header.ts";
|
|
9
9
|
import { canonicalJson, normalizeNewlines, serializeCanonical } from "../primitives/serialize.ts";
|
|
10
|
+
import { toSnakeCase } from "../data-graph/sql/naming.ts";
|
|
10
11
|
import { resolveByPackageName } from "../recipes/registry.ts";
|
|
11
12
|
import {
|
|
12
13
|
defaultRuntimeCompatibility,
|
|
@@ -185,6 +186,38 @@ function sourceText(workspaceRoot: string, file: string | undefined): string {
|
|
|
185
186
|
return nodeFileSystem.readText(absolute) ?? "";
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
function escapeRegExp(value: string): string {
|
|
190
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function addDbAliasesForText(
|
|
194
|
+
text: string,
|
|
195
|
+
tableNames: Set<string>,
|
|
196
|
+
aliases: Map<string, string>,
|
|
197
|
+
): void {
|
|
198
|
+
for (const match of text.matchAll(
|
|
199
|
+
/\b(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*ctx\.db(?:\.([A-Za-z_$][A-Za-z0-9_$]*)|\[\s*["'`]([^"'`]+)["'`]\s*\])/g,
|
|
200
|
+
)) {
|
|
201
|
+
const alias = match[1] ?? "";
|
|
202
|
+
const table = match[2] ?? match[3] ?? "";
|
|
203
|
+
if (alias && tableNames.has(table)) {
|
|
204
|
+
aliases.set(alias, table);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const match of text.matchAll(/\b(?:const|let|var)\s*\{([^}]+)\}\s*=\s*ctx\.db/g)) {
|
|
209
|
+
const body = match[1] ?? "";
|
|
210
|
+
for (const part of body.split(",")) {
|
|
211
|
+
const [rawTable, rawAlias] = part.split(":").map((value) => value.trim());
|
|
212
|
+
const table = rawTable?.replace(/["'`]/g, "") ?? "";
|
|
213
|
+
const alias = (rawAlias ?? rawTable ?? "").replace(/\s*=.*$/, "").trim();
|
|
214
|
+
if (tableNames.has(table) && alias) {
|
|
215
|
+
aliases.set(alias, table);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
188
221
|
function dbTablesForText(
|
|
189
222
|
text: string,
|
|
190
223
|
tableNames: Set<string>,
|
|
@@ -198,6 +231,24 @@ function dbTablesForText(
|
|
|
198
231
|
tables.push(table);
|
|
199
232
|
}
|
|
200
233
|
}
|
|
234
|
+
for (const match of text.matchAll(/ctx\.db\s*\[\s*["'`]([^"'`]+)["'`]\s*\]\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)/g)) {
|
|
235
|
+
const table = match[1] ?? "";
|
|
236
|
+
const op = match[2] ?? "";
|
|
237
|
+
if (tableNames.has(table) && ops.has(op)) {
|
|
238
|
+
tables.push(table);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const aliases = new Map<string, string>();
|
|
242
|
+
addDbAliasesForText(text, tableNames, aliases);
|
|
243
|
+
for (const [alias, table] of aliases) {
|
|
244
|
+
const aliasPattern = new RegExp(`\\b${escapeRegExp(alias)}\\s*\\.\\s*([A-Za-z_$][A-Za-z0-9_$]*)`, "g");
|
|
245
|
+
for (const match of text.matchAll(aliasPattern)) {
|
|
246
|
+
const op = match[1] ?? "";
|
|
247
|
+
if (ops.has(op)) {
|
|
248
|
+
tables.push(table);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
201
252
|
return uniqueSorted(tables);
|
|
202
253
|
}
|
|
203
254
|
|
|
@@ -288,6 +339,35 @@ function runtimeRules(): AgentRuntimeRule[] {
|
|
|
288
339
|
];
|
|
289
340
|
}
|
|
290
341
|
|
|
342
|
+
type TenantTableInfo = {
|
|
343
|
+
tenantIdColumn: string;
|
|
344
|
+
tenantField: string;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
function buildTenantTableLookup(tenantScope: TenantScope, dataGraph: DataGraph): Map<string, TenantTableInfo> {
|
|
348
|
+
const lookup = new Map<string, TenantTableInfo>();
|
|
349
|
+
for (const scoped of tenantScope.tables) {
|
|
350
|
+
const table = dataGraph.tables.find((candidate) =>
|
|
351
|
+
candidate.name === scoped.table ||
|
|
352
|
+
candidate.exportName === scoped.exportName ||
|
|
353
|
+
toSnakeCase(candidate.name) === scoped.table ||
|
|
354
|
+
toSnakeCase(candidate.exportName) === scoped.table
|
|
355
|
+
);
|
|
356
|
+
const tenantField = table?.fields.find((field) => toSnakeCase(field.name) === scoped.tenantIdColumn)?.name ??
|
|
357
|
+
scoped.tenantIdColumn;
|
|
358
|
+
const info = { tenantIdColumn: scoped.tenantIdColumn, tenantField };
|
|
359
|
+
for (const key of uniqueSorted([
|
|
360
|
+
scoped.table,
|
|
361
|
+
scoped.exportName,
|
|
362
|
+
table?.name,
|
|
363
|
+
table?.exportName,
|
|
364
|
+
].filter((key): key is string => typeof key === "string" && key.length > 0))) {
|
|
365
|
+
lookup.set(key, info);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return lookup;
|
|
369
|
+
}
|
|
370
|
+
|
|
291
371
|
function agentProtocols(workspaceRoot: string): AgentProtocolInfo[] {
|
|
292
372
|
return [
|
|
293
373
|
{
|
|
@@ -1037,9 +1117,7 @@ export function buildAgentContractArtifacts(
|
|
|
1037
1117
|
input: AgentContractInput,
|
|
1038
1118
|
): AgentContractArtifacts {
|
|
1039
1119
|
const project = readPackageInfo(input.workspaceRoot);
|
|
1040
|
-
const tenantTables =
|
|
1041
|
-
input.tenantScope.tables.map((table) => [table.table, table.tenantIdColumn]),
|
|
1042
|
-
);
|
|
1120
|
+
const tenantTables = buildTenantTableLookup(input.tenantScope, input.dataGraph);
|
|
1043
1121
|
const commandAuth = new Map(
|
|
1044
1122
|
input.policyRegistry.commandAuth.map((binding) => [binding.commandName, binding.auth]),
|
|
1045
1123
|
);
|
|
@@ -1183,13 +1261,16 @@ export function buildAgentContractArtifacts(
|
|
|
1183
1261
|
}),
|
|
1184
1262
|
),
|
|
1185
1263
|
data: {
|
|
1186
|
-
tables: sorted(input.dataGraph.tables, (table) => table.name).map((table) =>
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1264
|
+
tables: sorted(input.dataGraph.tables, (table) => table.name).map((table) => {
|
|
1265
|
+
const tenantInfo = tenantTables.get(table.name);
|
|
1266
|
+
return {
|
|
1267
|
+
name: table.name,
|
|
1268
|
+
file: table.file,
|
|
1269
|
+
tenantScoped: Boolean(tenantInfo),
|
|
1270
|
+
...(tenantInfo ? { tenantField: tenantInfo.tenantField } : {}),
|
|
1271
|
+
fields: uniqueSorted(table.fields.map((field) => field.name)),
|
|
1272
|
+
};
|
|
1273
|
+
}),
|
|
1193
1274
|
},
|
|
1194
1275
|
policies: sorted(input.policyRegistry.policies, (policy) => policy.name).map((policy) => ({
|
|
1195
1276
|
name: policy.name,
|
|
@@ -4,7 +4,7 @@ import type { ForgeCommand } from "../cli/parse.ts";
|
|
|
4
4
|
import { GENERATED_DIR } from "../compiler/emitter/constants.ts";
|
|
5
5
|
import { normalizePath } from "../compiler/primitives/paths.ts";
|
|
6
6
|
import { hashUtf8Bytes } from "../compiler/primitives/hash.ts";
|
|
7
|
-
import { DeltaStore, type DeltaRuntimeCallInput } from "./store.ts";
|
|
7
|
+
import { DeltaStore, DeltaStoreBusyError, type DeltaRuntimeCallInput } from "./store.ts";
|
|
8
8
|
import { classifyArtifactKind } from "./classifier.ts";
|
|
9
9
|
import { readDeltaGitSnapshot } from "./git-observer.ts";
|
|
10
10
|
|
|
@@ -28,70 +28,129 @@ export async function createAmbientDeltaRecorder(
|
|
|
28
28
|
if (isDeltaDisabled()) {
|
|
29
29
|
return noopRecorder;
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
let actorId: string | undefined;
|
|
32
|
+
let sessionId: string | undefined;
|
|
33
|
+
let accepting = true;
|
|
34
|
+
let closed = false;
|
|
35
|
+
let queue = Promise.resolve();
|
|
36
|
+
|
|
37
|
+
const withStore = async (fn: (store: DeltaStore) => Promise<void>): Promise<void> => {
|
|
38
|
+
const store = await openDeltaStoreWithRetry(workspaceRoot);
|
|
39
|
+
try {
|
|
40
|
+
await fn(store);
|
|
41
|
+
} finally {
|
|
42
|
+
await store.close();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const enqueue = (fn: (store: DeltaStore) => Promise<void>): Promise<void> => {
|
|
47
|
+
queue = queue.then(() => safeDelta(() => withStore(fn)));
|
|
48
|
+
return queue;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const ensureSession = async (store: DeltaStore): Promise<{ actorId: string; sessionId: string } | null> => {
|
|
52
|
+
if (actorId && sessionId) {
|
|
53
|
+
return { actorId, sessionId };
|
|
54
|
+
}
|
|
55
|
+
actorId = await store.ensureActor("forge", "forge-cli", { pid: process.pid });
|
|
56
|
+
sessionId = await store.createSession({
|
|
35
57
|
source,
|
|
36
58
|
summary,
|
|
37
59
|
metadata: { actorId },
|
|
38
60
|
git: readDeltaGitSnapshot(workspaceRoot),
|
|
39
61
|
});
|
|
40
|
-
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
},
|
|
79
|
-
});
|
|
62
|
+
return { actorId, sessionId };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
await enqueue(async (store) => {
|
|
66
|
+
await ensureSession(store);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
get sessionId() {
|
|
71
|
+
return sessionId;
|
|
72
|
+
},
|
|
73
|
+
async recordRuntimeCall(input) {
|
|
74
|
+
if (!accepting) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await enqueue(async (store) => {
|
|
78
|
+
const session = await ensureSession(store);
|
|
79
|
+
if (!session) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const failedCode = input.diagnosticCode ?? diagnosticCode(input.diagnostics);
|
|
83
|
+
await store.appendOperation({
|
|
84
|
+
sessionId: session.sessionId,
|
|
85
|
+
actorId: session.actorId,
|
|
86
|
+
kind: input.result === "denied"
|
|
87
|
+
? "runtime.entry.denied"
|
|
88
|
+
: input.result === "failed"
|
|
89
|
+
? "runtime.entry.failed"
|
|
90
|
+
: "runtime.entry.executed",
|
|
91
|
+
summary: `${input.entryName} ${input.result ?? "executed"}`,
|
|
92
|
+
data: {
|
|
93
|
+
entryName: input.entryName,
|
|
94
|
+
entryKind: input.entryKind,
|
|
95
|
+
result: input.result,
|
|
96
|
+
traceId: input.traceId,
|
|
97
|
+
diagnosticCode: failedCode,
|
|
98
|
+
},
|
|
99
|
+
runtimeCall: { ...input, diagnosticCode: failedCode },
|
|
80
100
|
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
async recordAgentTool(input) {
|
|
104
|
+
if (!accepting) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await enqueue(async (store) => {
|
|
108
|
+
const session = await ensureSession(store);
|
|
109
|
+
if (!session) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
await store.appendOperation({
|
|
113
|
+
sessionId: session.sessionId,
|
|
114
|
+
actorId: session.actorId,
|
|
115
|
+
kind: "agent.tool.called",
|
|
116
|
+
summary: `${input.toolName} ${input.status}`,
|
|
117
|
+
data: {
|
|
118
|
+
toolName: input.toolName,
|
|
119
|
+
risk: input.risk,
|
|
120
|
+
status: input.status,
|
|
121
|
+
traceId: input.traceId,
|
|
122
|
+
durationMs: input.durationMs,
|
|
123
|
+
},
|
|
89
124
|
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
async recordFileChanged(path, changeType = "modified") {
|
|
128
|
+
if (!accepting) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
await enqueue(async (store) => {
|
|
132
|
+
const session = await ensureSession(store);
|
|
133
|
+
if (!session) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
await store.recordFilePath(session.sessionId, path, changeType);
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
async close(closeSummary) {
|
|
140
|
+
if (closed) {
|
|
141
|
+
await queue;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
accepting = false;
|
|
145
|
+
closed = true;
|
|
146
|
+
await enqueue(async (store) => {
|
|
147
|
+
if (!sessionId) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
await store.endSession(sessionId, closeSummary);
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
};
|
|
95
154
|
}
|
|
96
155
|
|
|
97
156
|
export async function recordParsedCliCommand(input: {
|
|
@@ -343,6 +402,25 @@ const noopRecorder: AmbientDeltaRecorder = {
|
|
|
343
402
|
async close() {},
|
|
344
403
|
};
|
|
345
404
|
|
|
405
|
+
const DELTA_STORE_RETRY_DELAYS_MS = [25, 75, 150];
|
|
406
|
+
|
|
407
|
+
async function openDeltaStoreWithRetry(workspaceRoot: string): Promise<DeltaStore> {
|
|
408
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
409
|
+
try {
|
|
410
|
+
return await DeltaStore.open(workspaceRoot, { waitMs: 1_500, retryDelayMs: 50 });
|
|
411
|
+
} catch (error) {
|
|
412
|
+
if (!(error instanceof DeltaStoreBusyError) || attempt >= DELTA_STORE_RETRY_DELAYS_MS.length) {
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
await sleep(DELTA_STORE_RETRY_DELAYS_MS[attempt] ?? 0);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function sleep(ms: number): Promise<void> {
|
|
421
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
422
|
+
}
|
|
423
|
+
|
|
346
424
|
async function safeDelta(fn: () => Promise<void>): Promise<void> {
|
|
347
425
|
try {
|
|
348
426
|
await fn();
|
package/src/forge/delta/store.ts
CHANGED
|
@@ -348,6 +348,12 @@ interface DeltaStoreLock {
|
|
|
348
348
|
token: string;
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
export interface DeltaStoreOpenOptions {
|
|
352
|
+
access?: DeltaStoreAccess;
|
|
353
|
+
waitMs?: number;
|
|
354
|
+
retryDelayMs?: number;
|
|
355
|
+
}
|
|
356
|
+
|
|
351
357
|
function getDeltaLockPath(workspaceRoot: string): string {
|
|
352
358
|
return join(workspaceRoot, ".forge", "delta", "delta.lock");
|
|
353
359
|
}
|
|
@@ -488,6 +494,33 @@ function acquireDeltaStoreLock(workspaceRoot: string): DeltaStoreLock {
|
|
|
488
494
|
throw new DeltaStoreBusyError(lockPath, readLockHolder(lockPath));
|
|
489
495
|
}
|
|
490
496
|
|
|
497
|
+
function sleep(ms: number): Promise<void> {
|
|
498
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function acquireDeltaStoreLockWithWait(
|
|
502
|
+
workspaceRoot: string,
|
|
503
|
+
options: { waitMs?: number; retryDelayMs?: number } = {},
|
|
504
|
+
): Promise<DeltaStoreLock> {
|
|
505
|
+
const waitMs = Math.max(0, options.waitMs ?? 0);
|
|
506
|
+
const retryDelayMs = Math.max(10, options.retryDelayMs ?? 50);
|
|
507
|
+
const started = Date.now();
|
|
508
|
+
for (;;) {
|
|
509
|
+
try {
|
|
510
|
+
return acquireDeltaStoreLock(workspaceRoot);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
if (!(error instanceof DeltaStoreBusyError)) {
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
const elapsed = Date.now() - started;
|
|
516
|
+
if (elapsed >= waitMs) {
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
await sleep(Math.min(retryDelayMs, waitMs - elapsed));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
491
524
|
export function probeDeltaStoreBusy(workspaceRoot: string): DeltaStoreBusyError | null {
|
|
492
525
|
const lockPath = getDeltaLockPath(workspaceRoot);
|
|
493
526
|
if (!existsSync(lockPath)) {
|
|
@@ -551,11 +584,11 @@ export class DeltaStore {
|
|
|
551
584
|
private readonly lock: DeltaStoreLock | null,
|
|
552
585
|
) {}
|
|
553
586
|
|
|
554
|
-
static async open(workspaceRoot: string, options:
|
|
587
|
+
static async open(workspaceRoot: string, options: DeltaStoreOpenOptions = {}): Promise<DeltaStore> {
|
|
555
588
|
const storePath = getDeltaStorePath(workspaceRoot);
|
|
556
589
|
mkdirSync(dirname(storePath), { recursive: true });
|
|
557
590
|
const initializedBeforeOpen = deltaStoreInitialized(storePath);
|
|
558
|
-
const lock = options.access === "read" ? null :
|
|
591
|
+
const lock = options.access === "read" ? null : await acquireDeltaStoreLockWithWait(workspaceRoot, options);
|
|
559
592
|
let store: DeltaStore | null = null;
|
|
560
593
|
try {
|
|
561
594
|
const adapter = await createPgliteAdapter(storePath);
|
|
@@ -565,7 +598,7 @@ export class DeltaStore {
|
|
|
565
598
|
} else if (await store.needsSchemaInit()) {
|
|
566
599
|
await store.close();
|
|
567
600
|
store = null;
|
|
568
|
-
const migrateLock =
|
|
601
|
+
const migrateLock = await acquireDeltaStoreLockWithWait(workspaceRoot, options);
|
|
569
602
|
try {
|
|
570
603
|
const migrateAdapter = await createPgliteAdapter(storePath);
|
|
571
604
|
store = new DeltaStore(workspaceRoot, storePath, migrateAdapter, migrateLock);
|
package/src/forge/make/index.ts
CHANGED
|
@@ -200,6 +200,13 @@ function chooseSchemaFile(workspaceRoot: string): string {
|
|
|
200
200
|
return "src/forge/schema.ts";
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
function schemaHasTenantsTable(workspaceRoot: string): boolean {
|
|
204
|
+
const schema = readIfExists(workspaceRoot, chooseSchemaFile(workspaceRoot)) ?? "";
|
|
205
|
+
return schema.includes('name: "tenants"') ||
|
|
206
|
+
schema.includes("name: 'tenants'") ||
|
|
207
|
+
/\btenants\s*=\s*(?:defineTable|table)\s*\(/.test(schema);
|
|
208
|
+
}
|
|
209
|
+
|
|
203
210
|
function choosePolicyFile(workspaceRoot: string): string {
|
|
204
211
|
if (fileExists(workspaceRoot, "src/policies.ts")) {
|
|
205
212
|
return "src/policies.ts";
|
|
@@ -551,6 +558,8 @@ function buildIntent(options: MakeCommandOptions): {
|
|
|
551
558
|
kind === "command"
|
|
552
559
|
? actionName?.replace(/^(create|update|delete)/, "") || "create"
|
|
553
560
|
: "read";
|
|
561
|
+
const tenantScoped =
|
|
562
|
+
options.tenantScoped || (kind === "resource" && schemaHasTenantsTable(options.workspaceRoot));
|
|
554
563
|
|
|
555
564
|
return {
|
|
556
565
|
diagnostics,
|
|
@@ -560,7 +569,7 @@ function buildIntent(options: MakeCommandOptions): {
|
|
|
560
569
|
table,
|
|
561
570
|
field: fieldOptions.field,
|
|
562
571
|
fields,
|
|
563
|
-
tenantScoped
|
|
572
|
+
tenantScoped,
|
|
564
573
|
crud: options.withCrud || kind === "resource",
|
|
565
574
|
liveQuery: options.withLiveQuery || kind === "resource" || kind === "livequery",
|
|
566
575
|
react:
|
|
@@ -898,7 +907,7 @@ function buildPlan(options: MakeCommandOptions): MakePlan {
|
|
|
898
907
|
kind: "resource" as const,
|
|
899
908
|
name: options.name ?? "resource",
|
|
900
909
|
fields: [],
|
|
901
|
-
tenantScoped:
|
|
910
|
+
tenantScoped: false,
|
|
902
911
|
crud: true,
|
|
903
912
|
liveQuery: true,
|
|
904
913
|
react: true,
|
package/src/forge/version.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
2
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { normalizePath } from "../compiler/primitives/paths.ts";
|
|
4
5
|
import {
|
|
5
6
|
categorizeFiles,
|
|
6
7
|
classifyChangeType,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
|
|
14
15
|
export interface WorkspaceGitSummary {
|
|
15
16
|
available: boolean;
|
|
17
|
+
source?: "git" | "filesystem";
|
|
16
18
|
branch?: string;
|
|
17
19
|
commit?: string;
|
|
18
20
|
changed: FileListSummary;
|
|
@@ -28,18 +30,64 @@ export interface WorkspaceGitSummary {
|
|
|
28
30
|
error?: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
const FALLBACK_IGNORED_DIRS = new Set([
|
|
34
|
+
".git",
|
|
35
|
+
"node_modules",
|
|
36
|
+
"dist",
|
|
37
|
+
"build",
|
|
38
|
+
"coverage",
|
|
39
|
+
".next",
|
|
40
|
+
".nuxt",
|
|
41
|
+
".turbo",
|
|
42
|
+
".cache",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
function listWorkspaceFiles(root: string): string[] {
|
|
46
|
+
const files: string[] = [];
|
|
47
|
+
const visit = (dir: string): void => {
|
|
48
|
+
let entries: Array<{ name: string; isDirectory: () => boolean; isFile: () => boolean }> = [];
|
|
49
|
+
try {
|
|
50
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
51
|
+
} catch {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if (entry.isDirectory() && FALLBACK_IGNORED_DIRS.has(entry.name)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const absolute = join(dir, entry.name);
|
|
59
|
+
const rel = normalizePath(relative(root, absolute));
|
|
60
|
+
if (!rel || rel === ".") {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
visit(absolute);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (entry.isFile()) {
|
|
68
|
+
files.push(rel);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
visit(root);
|
|
73
|
+
return filterVolatileForgeState(files).sort();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function filesystemSummary(workspaceRoot: string, error?: string): WorkspaceGitSummary {
|
|
77
|
+
const files = listWorkspaceFiles(workspaceRoot);
|
|
78
|
+
const classify = workspaceChangeClassifier(workspaceRoot);
|
|
32
79
|
return {
|
|
33
80
|
available: false,
|
|
34
|
-
|
|
81
|
+
source: "filesystem",
|
|
82
|
+
changed: compactFiles(files),
|
|
35
83
|
staged: compactFiles([]),
|
|
36
84
|
unstaged: compactFiles([]),
|
|
37
|
-
untracked: compactFiles(
|
|
85
|
+
untracked: compactFiles(files),
|
|
38
86
|
changeSummary: {
|
|
39
|
-
changed: categorizeFiles(
|
|
87
|
+
changed: categorizeFiles(files, 8, classify),
|
|
40
88
|
staged: categorizeFiles([]),
|
|
41
89
|
unstaged: categorizeFiles([]),
|
|
42
|
-
untracked: categorizeFiles(
|
|
90
|
+
untracked: categorizeFiles(files, 8, classify),
|
|
43
91
|
},
|
|
44
92
|
...(error ? { error } : {}),
|
|
45
93
|
};
|
|
@@ -227,7 +275,7 @@ function parseStatusPath(line: string): string {
|
|
|
227
275
|
export function buildWorkspaceGitSummary(workspaceRoot: string): WorkspaceGitSummary {
|
|
228
276
|
const root = runGit(["rev-parse", "--show-toplevel"], workspaceRoot);
|
|
229
277
|
if (!root.ok) {
|
|
230
|
-
return
|
|
278
|
+
return filesystemSummary(workspaceRoot, root.error);
|
|
231
279
|
}
|
|
232
280
|
|
|
233
281
|
const status = runGit(["status", "--porcelain=v1", "-uall"], workspaceRoot, { trim: false });
|
|
@@ -262,6 +310,7 @@ export function buildWorkspaceGitSummary(workspaceRoot: string): WorkspaceGitSum
|
|
|
262
310
|
|
|
263
311
|
return {
|
|
264
312
|
available: true,
|
|
313
|
+
source: "git",
|
|
265
314
|
...(branch.ok ? { branch: branch.stdout } : {}),
|
|
266
315
|
...(commit.ok ? { commit: commit.stdout } : {}),
|
|
267
316
|
changed: compactFiles(changedFiles),
|