@nanobpm/urban 0.51.0 → 0.53.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/README.md +47 -20
- package/dist/runtime/core/events.d.ts +135 -0
- package/dist/runtime/core/events.js +329 -0
- package/dist/runtime/core/execContext.d.ts +5 -0
- package/dist/runtime/core/extensions.d.ts +131 -0
- package/dist/runtime/core/extensions.js +137 -0
- package/dist/runtime/core/glob.d.ts +7 -6
- package/dist/runtime/core/glob.js +16 -14
- package/dist/runtime/core/host.d.ts +4 -1
- package/dist/runtime/core/lineage.d.ts +116 -0
- package/dist/runtime/core/lineage.js +186 -0
- package/dist/runtime/core/modules/agent.d.ts +17 -0
- package/dist/runtime/core/modules/agent.js +63 -0
- package/dist/runtime/core/modules/datasource.d.ts +6 -0
- package/dist/runtime/core/modules/datasource.js +8 -0
- package/dist/runtime/core/modules/deploy.d.ts +8 -5
- package/dist/runtime/core/modules/deploy.js +62 -16
- package/dist/runtime/core/modules/lineage-store.d.ts +81 -0
- package/dist/runtime/core/modules/lineage-store.js +175 -0
- package/dist/runtime/core/modules/pages.js +397 -20
- package/dist/runtime/core/modules/surfaces.js +8 -0
- package/dist/runtime/core/modules/workers.js +69 -2
- package/dist/runtime/core/phase.d.ts +168 -0
- package/dist/runtime/core/phase.js +367 -0
- package/dist/runtime/core/runtime.d.ts +17 -0
- package/dist/runtime/core/runtime.js +30 -0
- package/dist/runtime/engine/nanosdk.js +8 -2
- package/dist/runtime/index.d.ts +10 -0
- package/dist/runtime/index.js +12 -0
- package/dist/toolkit/derivers/system-brief.d.ts +62 -0
- package/dist/toolkit/derivers/system-brief.js +156 -0
- package/dist/toolkit/gen.d.ts +5 -5
- package/dist/toolkit/gen.js +20 -14
- package/dist/toolkit/index.d.ts +5 -0
- package/dist/toolkit/index.js +3 -0
- package/dist/toolkit/models.d.ts +4 -3
- package/dist/toolkit/models.js +4 -3
- package/package.json +1 -1
- package/src/runtime/core/events.test.ts +382 -0
- package/src/runtime/core/events.ts +370 -0
- package/src/runtime/core/execContext.ts +5 -0
- package/src/runtime/core/extensions.test.ts +378 -0
- package/src/runtime/core/extensions.ts +276 -0
- package/src/runtime/core/glob.ts +15 -12
- package/src/runtime/core/host.ts +4 -1
- package/src/runtime/core/lineage.test.ts +215 -0
- package/src/runtime/core/lineage.ts +297 -0
- package/src/runtime/core/modules/agent.test.ts +104 -0
- package/src/runtime/core/modules/agent.ts +76 -0
- package/src/runtime/core/modules/datasource.ts +9 -0
- package/src/runtime/core/modules/deploy.test.ts +84 -16
- package/src/runtime/core/modules/deploy.ts +59 -16
- package/src/runtime/core/modules/lineage-store.test.ts +177 -0
- package/src/runtime/core/modules/lineage-store.ts +239 -0
- package/src/runtime/core/modules/pages.test.ts +309 -5
- package/src/runtime/core/modules/pages.ts +398 -20
- package/src/runtime/core/modules/surfaces.ts +9 -0
- package/src/runtime/core/modules/workers.test.ts +136 -1
- package/src/runtime/core/modules/workers.ts +72 -6
- package/src/runtime/core/phase.test.ts +350 -0
- package/src/runtime/core/phase.ts +526 -0
- package/src/runtime/core/runtime.extensions.test.ts +151 -0
- package/src/runtime/core/runtime.ts +49 -0
- package/src/runtime/devserver.test.ts +3 -0
- package/src/runtime/engine/nanosdk.test.ts +97 -3
- package/src/runtime/engine/nanosdk.ts +8 -2
- package/src/runtime/index.ts +92 -0
- package/src/toolkit/derivers/system-brief.test.ts +150 -0
- package/src/toolkit/derivers/system-brief.ts +210 -0
- package/src/toolkit/gen.test.ts +28 -4
- package/src/toolkit/gen.ts +20 -13
- package/src/toolkit/index.ts +14 -0
- package/src/toolkit/models.ts +4 -3
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Deriver: BPMN models + manifest → `system-brief.md` / `system-brief.json`, the app's
|
|
2
|
+
// *institutional-memory* brief (ADR 0060). Where `worker-io.ts` emits worker *types* and
|
|
3
|
+
// `meta.ts` emits a *typed accessor*, this deriver folds the SAME scanned structure into
|
|
4
|
+
// agent- and human-readable **system context**: the app's processes, its service-task call
|
|
5
|
+
// graph, the decisions it encodes, and the ownership / "why" carried on
|
|
6
|
+
// the model as reserved `nano:meta` keys. It reuses the sibling scanners (single parse rule
|
|
7
|
+
// per vocabulary) and is pure (models in, artifacts out) so `urban gen --check` keeps it
|
|
8
|
+
// honest — the brief can never rot the way a hand-written README does (ADR 0060 §1).
|
|
9
|
+
//
|
|
10
|
+
// Wired into `runGen` (gen.ts) as a standard artifact and served at runtime via `/app/agent`
|
|
11
|
+
// (agent.ts, ADR 0060 §2): the shape, scanners, fold, and both emitters are real and tested.
|
|
12
|
+
|
|
13
|
+
import type { DerivedArtifact, Deriver } from "../artifact.ts";
|
|
14
|
+
import { GENERATED_DIR } from "../artifact.ts";
|
|
15
|
+
import { byModelPath, type ModelSource, scanModelWorkers, type WorkerIo } from "./worker-io.ts";
|
|
16
|
+
import { scanModelMeta } from "./meta.ts";
|
|
17
|
+
|
|
18
|
+
/** Basenames of the two emitted artifacts. */
|
|
19
|
+
export const SYSTEM_BRIEF_MD = "system-brief.md";
|
|
20
|
+
export const SYSTEM_BRIEF_JSON = "system-brief.json";
|
|
21
|
+
|
|
22
|
+
/** Reserved `nano:meta` keys the brief recognises as ownership / provenance context (ADR 0060
|
|
23
|
+
* §1). All optional and additive — absence degrades the brief to structure-only. `adr` may be
|
|
24
|
+
* declared more than once (repeated `nano:meta`, matching the existing scan) to reference
|
|
25
|
+
* multiple ADRs; the scan collects them into `adrs[]`. */
|
|
26
|
+
export interface Ownership {
|
|
27
|
+
owner?: string;
|
|
28
|
+
team?: string;
|
|
29
|
+
slack?: string;
|
|
30
|
+
runbook?: string;
|
|
31
|
+
since?: string;
|
|
32
|
+
adrs: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** One decision the app encodes: a `businessRuleTask` with a `zeebe:calledDecision`. */
|
|
36
|
+
export interface DecisionRef {
|
|
37
|
+
process?: string;
|
|
38
|
+
elementId?: string;
|
|
39
|
+
decisionId: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The derived, machine-readable system model — the payload of `system-brief.json` and the
|
|
43
|
+
* input the runtime injects as session context / the retro plane consumes (ADR 0060 §3). */
|
|
44
|
+
export interface SystemBrief {
|
|
45
|
+
/** App id/name from the manifest, if provided. */
|
|
46
|
+
app?: string;
|
|
47
|
+
/** Every process id the models declare, in scan order. */
|
|
48
|
+
processes: string[];
|
|
49
|
+
/** Service-task call graph: each worker binding (task type + in/out envelope + process). The
|
|
50
|
+
* dependency edges are implicit — a process depends on the task types its service tasks invoke. */
|
|
51
|
+
workers: WorkerIo[];
|
|
52
|
+
/** Decisions (`businessRuleTask` → DMN) the models encode. */
|
|
53
|
+
decisions: DecisionRef[];
|
|
54
|
+
/** Ownership / provenance folded from reserved `nano:meta` keys. */
|
|
55
|
+
ownership: Ownership;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function attr(tag: string, name: string): string | undefined {
|
|
59
|
+
const m = tag.match(new RegExp(`\\b${name}\\s*=\\s*"([^"]*)"`));
|
|
60
|
+
return m ? m[1] : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function processId(xml: string): string | undefined {
|
|
64
|
+
const m = xml.match(/<bpmn:process\b[^>]*>/);
|
|
65
|
+
return m ? attr(m[0], "id") : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Scan one BPMN document for `businessRuleTask` → `zeebe:calledDecision` decision refs, in
|
|
69
|
+
* document order, tagged with the enclosing process (mirrors `scanModelWorkers`' shape). */
|
|
70
|
+
export function scanModelDecisions(xml: string): DecisionRef[] {
|
|
71
|
+
const proc = processId(xml);
|
|
72
|
+
const out: DecisionRef[] = [];
|
|
73
|
+
const blockRe = /<bpmn:businessRuleTask\b([^>]*)>([\s\S]*?)<\/bpmn:businessRuleTask>/g;
|
|
74
|
+
let m: RegExpExecArray | null;
|
|
75
|
+
while ((m = blockRe.exec(xml)) !== null) {
|
|
76
|
+
const elementId = attr(`<x ${m[1]}>`, "id");
|
|
77
|
+
const cd = m[2].match(/<zeebe:calledDecision\b[^>]*>/);
|
|
78
|
+
if (!cd) continue;
|
|
79
|
+
const decisionId = attr(cd[0], "decisionId");
|
|
80
|
+
if (!decisionId) continue;
|
|
81
|
+
out.push({ process: proc, elementId, decisionId });
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Reserved single-valued ownership keys → their `Ownership` field. */
|
|
87
|
+
const OWNERSHIP_KEYS: Record<string, Exclude<keyof Ownership, "adrs">> = {
|
|
88
|
+
owner: "owner",
|
|
89
|
+
team: "team",
|
|
90
|
+
slack: "slack",
|
|
91
|
+
runbook: "runbook",
|
|
92
|
+
since: "since",
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Fold the models' reserved `nano:meta` entries into `Ownership`. Single-valued keys take the
|
|
96
|
+
* last (scan-order) write, matching `foldMeta`; `adr` is multi-valued and accumulates. */
|
|
97
|
+
export function foldOwnership(models: ModelSource[]): Ownership {
|
|
98
|
+
const own: Ownership = { adrs: [] };
|
|
99
|
+
for (const model of [...models].sort(byModelPath)) {
|
|
100
|
+
for (const { key, value } of scanModelMeta(model.xml)) {
|
|
101
|
+
const k = key.trim().toLowerCase();
|
|
102
|
+
if (k === "adr") {
|
|
103
|
+
if (value && !own.adrs.includes(value)) own.adrs.push(value);
|
|
104
|
+
} else if (Object.hasOwn(OWNERSHIP_KEYS, k)) {
|
|
105
|
+
own[OWNERSHIP_KEYS[k]] = value ?? "";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return own;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Fold all models + manifest into the machine-readable `SystemBrief`. Deterministic: models
|
|
113
|
+
* are scanned in path order. */
|
|
114
|
+
export function buildSystemBrief(models: ModelSource[], app?: string): SystemBrief {
|
|
115
|
+
const sorted = [...models].sort(byModelPath);
|
|
116
|
+
const workers: WorkerIo[] = [];
|
|
117
|
+
const decisions: DecisionRef[] = [];
|
|
118
|
+
const processes: string[] = [];
|
|
119
|
+
for (const model of sorted) {
|
|
120
|
+
const proc = processId(model.xml);
|
|
121
|
+
if (proc && !processes.includes(proc)) processes.push(proc);
|
|
122
|
+
workers.push(...scanModelWorkers(model.xml));
|
|
123
|
+
decisions.push(...scanModelDecisions(model.xml));
|
|
124
|
+
}
|
|
125
|
+
return { app, processes, workers, decisions, ownership: foldOwnership(models) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Escape a Markdown table cell: neutralise `|` and newlines, which would otherwise break the
|
|
129
|
+
* table. Ownership values come from model-authored `nano:meta`, so this both keeps rendering
|
|
130
|
+
* honest and blunts Markdown/prompt injection into the agent-facing brief. Backticks are left
|
|
131
|
+
* intact — the emitter deliberately wraps ids in code spans. */
|
|
132
|
+
function mdCell(s: string): string {
|
|
133
|
+
return s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function mdTable(headers: string[], rows: string[][]): string {
|
|
137
|
+
if (rows.length === 0) return "_none_\n";
|
|
138
|
+
const head = `| ${headers.join(" | ")} |\n| ${headers.map(() => "---").join(" | ")} |\n`;
|
|
139
|
+
return head + rows.map((r) => `| ${r.map((c) => mdCell(c || "")).join(" | ")} |`).join("\n") + "\n";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Emit the agent- and human-readable `system-brief.md` from the machine model. Compact by
|
|
143
|
+
* design (call graph + owners, not model XML) to keep the per-session prompt budget small
|
|
144
|
+
* (ADR 0060 §3 / open question 1). */
|
|
145
|
+
export function emitSystemBriefMd(b: SystemBrief): string {
|
|
146
|
+
const o = b.ownership;
|
|
147
|
+
let m = `# ${b.app ?? "App"} — system brief\n\n`;
|
|
148
|
+
m += "_Auto-derived from the process models (ADR 0060). Do not edit — regenerated by " +
|
|
149
|
+
"`urban gen`; drift fails `urban gen --check`._\n\n";
|
|
150
|
+
|
|
151
|
+
m += "## Ownership\n\n";
|
|
152
|
+
const ownRows = [
|
|
153
|
+
["Owner", o.owner ?? ""],
|
|
154
|
+
["Team", o.team ?? ""],
|
|
155
|
+
["Slack", o.slack ?? ""],
|
|
156
|
+
["Runbook", o.runbook ?? ""],
|
|
157
|
+
["Since", o.since ?? ""],
|
|
158
|
+
["ADRs", o.adrs.join(", ")],
|
|
159
|
+
].filter((r) => r[1]);
|
|
160
|
+
m += ownRows.length ? mdTable(["Field", "Value"], ownRows) : "_No ownership metadata declared " +
|
|
161
|
+
"(add reserved `nano:meta` keys: owner, team, slack, runbook, adr, since)._\n";
|
|
162
|
+
m += "\n";
|
|
163
|
+
|
|
164
|
+
m += "## Processes\n\n";
|
|
165
|
+
m += b.processes.length ? b.processes.map((p) => `- \`${p}\``).join("\n") + "\n" : "_none_\n";
|
|
166
|
+
m += "\n";
|
|
167
|
+
|
|
168
|
+
m += "## Service-task call graph (dependency edges)\n\n";
|
|
169
|
+
m += mdTable(
|
|
170
|
+
["Process", "Task type (dependency)", "In", "Out"],
|
|
171
|
+
b.workers.map((w) => [
|
|
172
|
+
w.process ? `\`${w.process}\`` : "",
|
|
173
|
+
`\`${w.taskType}\``,
|
|
174
|
+
w.in ? `\`${w.in}\`` : "",
|
|
175
|
+
w.out ? `\`${w.out}\`` : "",
|
|
176
|
+
]),
|
|
177
|
+
);
|
|
178
|
+
m += "\n";
|
|
179
|
+
|
|
180
|
+
m += "## Decisions (DMN)\n\n";
|
|
181
|
+
m += mdTable(
|
|
182
|
+
["Process", "Element", "Decision"],
|
|
183
|
+
b.decisions.map((d) => [
|
|
184
|
+
d.process ? `\`${d.process}\`` : "",
|
|
185
|
+
d.elementId ? `\`${d.elementId}\`` : "",
|
|
186
|
+
`\`${d.decisionId}\``,
|
|
187
|
+
]),
|
|
188
|
+
);
|
|
189
|
+
return m;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Derive `nano-generated/system-brief.md` + `system-brief.json` from BPMN models (ADR 0060 §1).
|
|
194
|
+
* Pure and deterministic (path-ordered scan), so it is a drop-in drift-checkable artifact like
|
|
195
|
+
* its sibling derivers.
|
|
196
|
+
*/
|
|
197
|
+
export function deriveSystemBrief(models: ModelSource[], app?: string): DerivedArtifact[] {
|
|
198
|
+
const brief = buildSystemBrief(models, app);
|
|
199
|
+
return [
|
|
200
|
+
{ path: `${GENERATED_DIR}/${SYSTEM_BRIEF_MD}`, content: emitSystemBriefMd(brief) },
|
|
201
|
+
{ path: `${GENERATED_DIR}/${SYSTEM_BRIEF_JSON}`, content: JSON.stringify(brief, null, 2) + "\n" },
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The `Deriver` binding, peer to `worker-io` / `meta`, for wiring into `gen.ts`. */
|
|
206
|
+
export const systemBriefDeriver: Deriver<{ models: ModelSource[]; app?: string }> = {
|
|
207
|
+
id: "system-brief",
|
|
208
|
+
describe: "App institutional-memory brief (processes, call graph, decisions, ownership) — ADR 0060",
|
|
209
|
+
derive: ({ models, app }) => deriveSystemBrief(models, app),
|
|
210
|
+
};
|
package/src/toolkit/gen.test.ts
CHANGED
|
@@ -74,7 +74,7 @@ function fixture(): Record<string, string> {
|
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
test("runGen writes migrations, the worker index, the meta accessor, and the
|
|
77
|
+
test("runGen writes migrations, the worker index, the meta accessor, the message map, and the system brief", async () => {
|
|
78
78
|
const io = memIO(fixture());
|
|
79
79
|
const res = await runGen({ root: "/app", io });
|
|
80
80
|
const paths = res.artifacts.map((a) => a.path).sort();
|
|
@@ -83,6 +83,8 @@ test("runGen writes migrations, the worker index, the meta accessor, and the mes
|
|
|
83
83
|
"nano-generated/domain-rows.d.ts",
|
|
84
84
|
"nano-generated/message-io.d.ts",
|
|
85
85
|
"nano-generated/meta.ts",
|
|
86
|
+
"nano-generated/system-brief.json",
|
|
87
|
+
"nano-generated/system-brief.md",
|
|
86
88
|
"nano-generated/worker-io.d.ts",
|
|
87
89
|
]);
|
|
88
90
|
assert.ok(io.files["/app/nano-generated/app.schema.sql"].includes("CREATE TABLE"));
|
|
@@ -91,6 +93,27 @@ test("runGen writes migrations, the worker index, the meta accessor, and the mes
|
|
|
91
93
|
assert.ok(io.files["/app/nano-generated/message-io.d.ts"].includes("export type MessageName"));
|
|
92
94
|
assert.ok(io.files["/app/nano-generated/domain-rows.d.ts"].includes('"greetings": Greetings;'));
|
|
93
95
|
assert.ok(io.files["/app/nano-generated/domain-rows.d.ts"].includes('"greeting": {'));
|
|
96
|
+
// The system brief threads the app id from the manifest and folds the process + call graph.
|
|
97
|
+
assert.ok(io.files["/app/nano-generated/system-brief.md"].includes("# demo — system brief"));
|
|
98
|
+
assert.ok(io.files["/app/nano-generated/system-brief.md"].includes("demo.do"));
|
|
99
|
+
const brief = JSON.parse(io.files["/app/nano-generated/system-brief.json"]);
|
|
100
|
+
assert.equal(brief.app, "demo");
|
|
101
|
+
assert.deepEqual(brief.processes, ["p"]);
|
|
102
|
+
assert.equal(brief.workers[0].taskType, "demo.do");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("the system brief is drift-checked and swept like its sibling artifacts", async () => {
|
|
106
|
+
const io = memIO(fixture());
|
|
107
|
+
await runGen({ root: "/app", io });
|
|
108
|
+
// Drift gate owns the brief: a hand-edit is reported by --check.
|
|
109
|
+
io.files["/app/nano-generated/system-brief.json"] = "{ tampered: true }";
|
|
110
|
+
const check = await runGen({ root: "/app", io, check: true });
|
|
111
|
+
assert.ok(check.drift.includes("nano-generated/system-brief.json"), "brief drift is reported");
|
|
112
|
+
// Stale sweep owns the brief: a renamed-away brief artifact is removed on the next write.
|
|
113
|
+
io.files["/app/nano-generated/system-brief.old.md"] = "# stale brief";
|
|
114
|
+
const res = await runGen({ root: "/app", io });
|
|
115
|
+
assert.ok(res.swept.includes("nano-generated/system-brief.old.md"), "stale brief swept");
|
|
116
|
+
assert.ok("/app/nano-generated/system-brief.md" in io.files, "fresh brief kept");
|
|
94
117
|
});
|
|
95
118
|
|
|
96
119
|
test("gen --check reports no drift right after a write", async () => {
|
|
@@ -345,13 +368,14 @@ test("readModels: a models block without processes suppresses the resources/ con
|
|
|
345
368
|
const io = memIO({
|
|
346
369
|
"/app/resources/a.bpmn": "<bpmn:definitions/>",
|
|
347
370
|
"/app/resources/sub/b.bpmn": "<bpmn:definitions/>",
|
|
371
|
+
"/app/resources/sub/deeper/c.bpmn": "<bpmn:definitions/>",
|
|
348
372
|
});
|
|
349
|
-
// No models block at all → convention scans resources/ (
|
|
373
|
+
// No models block at all → convention scans resources/ recursively (issue #231).
|
|
350
374
|
const byConvention = await readModels("/app", io, {});
|
|
351
375
|
assert.deepEqual(
|
|
352
376
|
byConvention.map((m) => m.path).sort(),
|
|
353
|
-
["resources/a.bpmn", "resources/sub/b.bpmn"],
|
|
354
|
-
"with no models block, gen discovers under resources/ by convention",
|
|
377
|
+
["resources/a.bpmn", "resources/sub/b.bpmn", "resources/sub/deeper/c.bpmn"],
|
|
378
|
+
"with no models block, gen discovers under resources/ recursively by convention",
|
|
355
379
|
);
|
|
356
380
|
// A declared models block (even one that resolves to zero process files) is an explicit override:
|
|
357
381
|
// the convention scan must be skipped entirely, matching runtime deployModels.
|
package/src/toolkit/gen.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { deriveMeta } from "./derivers/meta.ts";
|
|
|
12
12
|
import { deriveMessageBindings } from "./derivers/messages.ts";
|
|
13
13
|
import { resolveShapes, scanModelShapes } from "./derivers/shapes.ts";
|
|
14
14
|
import { deriveApi } from "./derivers/api.ts";
|
|
15
|
+
import { deriveSystemBrief } from "./derivers/system-brief.ts";
|
|
15
16
|
import { parseSpec, sharedRequestBodySchemas } from "../openapi/spec.ts";
|
|
16
17
|
import { deriveModels, type DerivedModels, type ModelError, MODEL_PROVENANCE } from "./models.ts";
|
|
17
18
|
|
|
@@ -106,31 +107,32 @@ export async function expandPattern(root: string, io: GenIO, pattern: string): P
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
/** The deploy-by-convention directory (ADR 0062): when the manifest declares no `models`, both the
|
|
109
|
-
* runtime deploy and this codegen scan discover deployables/models under it —
|
|
110
|
-
*
|
|
110
|
+
* runtime deploy and this codegen scan discover deployables/models under it — **recursively**,
|
|
111
|
+
* every file at any depth (issue #231). Deploy-only by convention. */
|
|
111
112
|
export const RESOURCES_DIR = "resources";
|
|
112
113
|
|
|
113
114
|
/** Model file extensions the `nano:shape`/code-first scan reads by convention (BPMN + DMN). */
|
|
114
115
|
const MODEL_EXTS = [".bpmn", ".dmn"];
|
|
115
116
|
|
|
116
117
|
/**
|
|
117
|
-
* Discover model files under `resources/` by convention —
|
|
118
|
-
*
|
|
119
|
-
* and the deploy see the same set. Returns root-relative paths, sorted. Descending
|
|
118
|
+
* Discover model files under `resources/` by convention — **recursively**, every `.bpmn`/`.dmn`
|
|
119
|
+
* under `resources/` at any depth (issue #231) — mirroring the runtime deploy walk so the codegen
|
|
120
|
+
* scan and the deploy see the same set. Returns root-relative paths, sorted. Descending needs
|
|
120
121
|
* `io.listSubdirs`; without it only the files directly under `resources/` are scanned.
|
|
121
122
|
*/
|
|
122
123
|
export async function discoverResourceModels(root: string, io: GenIO): Promise<string[]> {
|
|
123
124
|
const isModel = (n: string): boolean => MODEL_EXTS.some((e) => n.endsWith(e));
|
|
124
125
|
const out: string[] = [];
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const subdirs = io.listSubdirs ? await io.listSubdirs(join(root, RESOURCES_DIR)) : [];
|
|
129
|
-
for (const sub of subdirs.slice().sort()) {
|
|
130
|
-
for (const n of await io.listDir(join(root, `${RESOURCES_DIR}/${sub}`))) {
|
|
131
|
-
if (isModel(n)) out.push(`${RESOURCES_DIR}/${sub}/${n}`);
|
|
126
|
+
const walk = async (rel: string): Promise<void> => {
|
|
127
|
+
for (const n of await io.listDir(join(root, rel))) {
|
|
128
|
+
if (isModel(n)) out.push(`${rel}/${n}`);
|
|
132
129
|
}
|
|
133
|
-
|
|
130
|
+
const subdirs = io.listSubdirs ? await io.listSubdirs(join(root, rel)) : [];
|
|
131
|
+
for (const sub of subdirs.slice().sort()) {
|
|
132
|
+
await walk(`${rel}/${sub}`);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
await walk(RESOURCES_DIR);
|
|
134
136
|
return out.sort();
|
|
135
137
|
}
|
|
136
138
|
|
|
@@ -233,6 +235,11 @@ async function collectAll(opts: GenOptions): Promise<{ artifacts: DerivedArtifac
|
|
|
233
235
|
artifacts.push(...deriveWorkerBindings(models, declaredTypeIds));
|
|
234
236
|
artifacts.push(...deriveMeta(models));
|
|
235
237
|
artifacts.push(...deriveMessageBindings(models, declaredTypeIds));
|
|
238
|
+
// Institutional-memory brief (ADR 0060): fold the same scanned model structure into the
|
|
239
|
+
// app's agent/human-readable system brief. Flows through `artifacts` like its siblings, so
|
|
240
|
+
// the `nano-generated/` stale sweep and the `urban gen --check` drift gate own the two new
|
|
241
|
+
// artifacts (system-brief.md / .json) with no extra bookkeeping.
|
|
242
|
+
artifacts.push(...deriveSystemBrief(models, manifest.id));
|
|
236
243
|
// Surface genuine data-envelope conflicts: a taskType whose variants disagree on a field's
|
|
237
244
|
// TYPE. The emitted contract is the union of the variants (sound for a shared handler), so a
|
|
238
245
|
// type clash would silently widen to `A | B` — warn instead so the author unifies the envelope.
|
package/src/toolkit/index.ts
CHANGED
|
@@ -110,6 +110,18 @@ export {
|
|
|
110
110
|
API_CONTROLLER_TS,
|
|
111
111
|
} from "./derivers/api.ts";
|
|
112
112
|
|
|
113
|
+
export {
|
|
114
|
+
buildSystemBrief,
|
|
115
|
+
deriveSystemBrief,
|
|
116
|
+
emitSystemBriefMd,
|
|
117
|
+
foldOwnership,
|
|
118
|
+
scanModelDecisions,
|
|
119
|
+
systemBriefDeriver,
|
|
120
|
+
SYSTEM_BRIEF_JSON,
|
|
121
|
+
SYSTEM_BRIEF_MD,
|
|
122
|
+
} from "./derivers/system-brief.ts";
|
|
123
|
+
export type { DecisionRef, Ownership, SystemBrief } from "./derivers/system-brief.ts";
|
|
124
|
+
|
|
113
125
|
// The OpenAPI machinery the deriver + runtime share (ADR 0058).
|
|
114
126
|
export {
|
|
115
127
|
collectOperations,
|
|
@@ -139,6 +151,7 @@ import { workerIoDeriver } from "./derivers/worker-io.ts";
|
|
|
139
151
|
import { metaDeriver } from "./derivers/meta.ts";
|
|
140
152
|
import { messagesDeriver } from "./derivers/messages.ts";
|
|
141
153
|
import { apiDeriver } from "./derivers/api.ts";
|
|
154
|
+
import { systemBriefDeriver } from "./derivers/system-brief.ts";
|
|
142
155
|
export const DERIVERS = [
|
|
143
156
|
migrationsDeriver,
|
|
144
157
|
domainDeriver,
|
|
@@ -146,6 +159,7 @@ export const DERIVERS = [
|
|
|
146
159
|
metaDeriver,
|
|
147
160
|
messagesDeriver,
|
|
148
161
|
apiDeriver,
|
|
162
|
+
systemBriefDeriver,
|
|
149
163
|
] as const;
|
|
150
164
|
|
|
151
165
|
// Gen orchestrator + IO
|
package/src/toolkit/models.ts
CHANGED
|
@@ -21,9 +21,10 @@ import { isRecord } from "../runtime/core/guards.ts";
|
|
|
21
21
|
export const DEFAULT_WORKFLOW_PATTERNS = ["workflows/*.ts"];
|
|
22
22
|
|
|
23
23
|
/** Default output dir for derived models when the manifest declares no `models.processes`: under
|
|
24
|
-
* the deploy-by-convention `resources/` tree (ADR 0062),
|
|
25
|
-
*
|
|
26
|
-
* `processesOutDir` derives the dir from that glob instead (the
|
|
24
|
+
* the deploy-by-convention `resources/` tree (ADR 0062), so a derived model lands where the
|
|
25
|
+
* convention deploy + codegen scan (which walk `resources/` recursively) look for it. When
|
|
26
|
+
* `models.processes` IS set, `processesOutDir` derives the dir from that glob instead (the
|
|
27
|
+
* model-first override). */
|
|
27
28
|
export const PROCESSES_DIR = "resources/processes";
|
|
28
29
|
|
|
29
30
|
/** Marker stamped into every derived `.bpmn` (an XML comment) so re-gen overwrites and the
|