@nanobpm/urban 0.77.3 → 0.78.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/dist/runtime/core/modules/workers.js +28 -0
- package/dist/runtime/core/read-model.d.ts +267 -0
- package/dist/runtime/core/read-model.js +792 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +13 -0
- package/dist/toolkit/addConnector.js +8 -2
- package/dist/toolkit/scaffold/operations.js +11 -6
- package/dist/toolkit/scaffold/workers.js +12 -7
- package/dist/toolkit/scaffold.d.ts +12 -0
- package/dist/toolkit/scaffold.js +27 -2
- package/package.json +2 -2
- package/src/runtime/core/modules/workers.ts +28 -0
- package/src/runtime/core/read-model.test.ts +943 -0
- package/src/runtime/core/read-model.ts +1025 -0
- package/src/runtime/index.ts +65 -0
- package/src/toolkit/addConnector.test.ts +18 -0
- package/src/toolkit/addConnector.ts +8 -2
- package/src/toolkit/derivers/generated-lint.test.ts +2 -2
- package/src/toolkit/scaffold/operations.test.ts +43 -0
- package/src/toolkit/scaffold/operations.ts +11 -6
- package/src/toolkit/scaffold/workers.test.ts +53 -1
- package/src/toolkit/scaffold/workers.ts +12 -7
- package/src/toolkit/scaffold.ts +27 -2
package/dist/runtime/index.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export { applyAmbientLineage, buildLineageTree, deriveLineage, LINEAGE_KEY, LINE
|
|
|
40
40
|
export type { ApplyLineageOptions, LineageAttachment, LineageEdge, LineageEdgeType, LineageEnvelope, LineageNode, LineageTree, } from "./core/lineage.ts";
|
|
41
41
|
export { LINEAGE_ATTACHMENTS_TABLE, LINEAGE_EDGES_TABLE, LINEAGE_SCHEMA_SQL, LineageStore, systemClock as lineageSystemClock, } from "./core/modules/lineage-store.ts";
|
|
42
42
|
export type { Clock as LineageClock, LineageJobLike, LineageStoreOptions, } from "./core/modules/lineage-store.ts";
|
|
43
|
+
export { and, assertReadModelParity, assertSqlIdentifier, caseWhen, col, compileToFn, compileToSqlSelect, defineReadModel, deriveReadModelViewDdl, eq, exists, gt, gte, lit, lt, lte, neq, not, or, pcol, ProjectionRegistry, projectionRegistry, ReadModelRegistry, readModelRegistry, when, } from "./core/read-model.ts";
|
|
44
|
+
export type { AndExpr, BaseRow, CaseExpr, ColExpr, CompareExpr, CompareOp, DerivationFn, ExistsExpr, Expr, Literal, LitExpr, NotExpr, OrExpr, ParityDb, ParityOptions, ParitySample, ProjColExpr, ProjectionRows, ProjectionSource, ReadModel, ReadModelDecl, SqlCompileOptions, WhenClause, } from "./core/read-model.ts";
|
|
43
45
|
export { buildScopeIndex, derivePhase, deriveInstancePhases, furthestReached, rollupLineagePhase, SCOPE_CONTAINERS, } from "./core/phase.ts";
|
|
44
46
|
export type { CrumbKind, CrumbSource, DerivePhaseOptions, FurthestReached, InstancePhase, LineagePhaseRollup, Phase, PhaseCrumb, PhaseGranularity, ProvenanceProgressRow, ScopeElement, ScopeIndex, } from "./core/phase.ts";
|
|
45
47
|
export { deployModels, RESOURCES_DIR } from "./core/modules/deploy.ts";
|
package/dist/runtime/index.js
CHANGED
|
@@ -40,6 +40,19 @@ export { evalCorrelation } from "./core/modules/triggers.js";
|
|
|
40
40
|
// progress (a root request → the instances/PRs/tasks it causes).
|
|
41
41
|
export { applyAmbientLineage, buildLineageTree, deriveLineage, LINEAGE_KEY, LINEAGE_NAMESPACE, mintRootRequestKey, readLineage, writeLineage, } from "./core/lineage.js";
|
|
42
42
|
export { LINEAGE_ATTACHMENTS_TABLE, LINEAGE_EDGES_TABLE, LINEAGE_SCHEMA_SQL, LineageStore, systemClock as lineageSystemClock, } from "./core/modules/lineage-store.js";
|
|
43
|
+
// ─────────────────────────────────────────── Read models ────────────────────────────────────────
|
|
44
|
+
// The declare-once, compile-to-both derived read-model primitive (ADR 0065): a closed expression DSL
|
|
45
|
+
// (`col`/`lit`/comparisons/`and`/`or`/`not`/`caseWhen`/`exists`) declared ONCE via `defineReadModel`,
|
|
46
|
+
// compiled to BOTH a SQLite VIEW select-list (`compileToSqlSelect`) and an in-process TS function
|
|
47
|
+
// (`compileToFn`) from the SAME AST, plus the framework-derived managed VIEW DDL and the parity guard
|
|
48
|
+
// that replaces per-projection hand-written parity tests.
|
|
49
|
+
//
|
|
50
|
+
// SIBLING REGISTRATION POINTS (ADR 0065 rollout — see core/read-model.ts for the full seam docs):
|
|
51
|
+
// • `readModelRegistry` — register a `defineReadModel(...)` result; the boot path (core/modules/
|
|
52
|
+
// workers.ts) applies its managed VIEW. The `writer-source-inversion` task registers here.
|
|
53
|
+
// • `projectionRegistry` — register a canonical engine-truth projection NAME usable in `exists(...)`.
|
|
54
|
+
// The `canonical-projections` task registers `urban_open_user_tasks` / `urban_instance_state`.
|
|
55
|
+
export { and, assertReadModelParity, assertSqlIdentifier, caseWhen, col, compileToFn, compileToSqlSelect, defineReadModel, deriveReadModelViewDdl, eq, exists, gt, gte, lit, lt, lte, neq, not, or, pcol, ProjectionRegistry, projectionRegistry, ReadModelRegistry, readModelRegistry, when, } from "./core/read-model.js";
|
|
43
56
|
// Phase — the framework-level *domain phase* primitive (issue #266): derive "which phase is this
|
|
44
57
|
// instance in?" from the BPMN scope hierarchy + write-provenance (Tier 0, zero declaration), an
|
|
45
58
|
// optional `nano:phase` override (Tier 1), and a lineage rollup for the epic frontier (Tier 2).
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// package (a subprocess) is the CLI's job, so this stays runtime-agnostic and
|
|
13
13
|
// unit-testable.
|
|
14
14
|
import { errorMessage, isRecord } from "../runtime/core/guards.js";
|
|
15
|
+
import { detectJsonIndent } from "./scaffold.js";
|
|
15
16
|
const MANIFEST_FILE = "nano-ide.ext.json";
|
|
16
17
|
function joinPath(a, b) {
|
|
17
18
|
// Trim both slash styles (as toolkit/gen.ts joinPath does) so a Windows-style
|
|
@@ -61,9 +62,11 @@ export async function addConnector(opts) {
|
|
|
61
62
|
if (!(await io.exists(appManifestPath))) {
|
|
62
63
|
throw new Error(`no ${manifestFile} at ${appManifestPath} — run this inside an Urban app.`);
|
|
63
64
|
}
|
|
65
|
+
let appRaw;
|
|
64
66
|
let app;
|
|
65
67
|
try {
|
|
66
|
-
|
|
68
|
+
appRaw = await io.readText(appManifestPath);
|
|
69
|
+
app = JSON.parse(appRaw);
|
|
67
70
|
}
|
|
68
71
|
catch (err) {
|
|
69
72
|
throw new Error(`failed to parse ${appManifestPath}: ${errorMessage(err)}`);
|
|
@@ -161,7 +164,10 @@ export async function addConnector(opts) {
|
|
|
161
164
|
workers.push(entry);
|
|
162
165
|
wired.push({ taskType: spec.type, alreadyPresent: false });
|
|
163
166
|
}
|
|
164
|
-
|
|
167
|
+
// Preserve the manifest's own indentation (default tab, matching the scaffold's Biome config)
|
|
168
|
+
// so enabling a connector never reformats nano.app.json out from under `npm run lint` — the same
|
|
169
|
+
// fidelity `urban gen` gives via detectJsonIndent, kept as the single source of truth.
|
|
170
|
+
await io.writeText(appManifestPath, `${JSON.stringify(app, null, detectJsonIndent(appRaw))}\n`);
|
|
165
171
|
return {
|
|
166
172
|
packId: pack.id,
|
|
167
173
|
wired,
|
|
@@ -29,16 +29,21 @@ export function renderOperationStub(operationId, dir = DEFAULT_OPERATIONS_DIR) {
|
|
|
29
29
|
`// (ADR 0059). This file is yours to edit — \`urban gen\` will never overwrite it. The\n` +
|
|
30
30
|
`// generated controller (nano-generated/controller.ts) imports this default export and\n` +
|
|
31
31
|
`// type-checks it against the spec, so the signature cannot drift. Implement the body below\n` +
|
|
32
|
-
`// (the validated \`{ req, params, query, body }\` input + the injected \`app\`)
|
|
32
|
+
`// (the validated \`{ req, params, query, body }\` input + the injected \`app\`), replacing the\n` +
|
|
33
|
+
`// guard warn + throw.\n` +
|
|
33
34
|
`import { NotImplemented } from "@nanobpm/urban";\n` +
|
|
34
35
|
`import { defineOperation } from ${JSON.stringify(`${up}nano-generated/operations.ts`)};\n` +
|
|
35
36
|
`\n` +
|
|
36
37
|
`export default defineOperation(${key}, async (input, app) => {\n` +
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
`\t// \`input\` is the validated request (params/query/body/req); reach app state + services\n` +
|
|
39
|
+
`\t// through the injected \`app\` API. Structured logs via \`app.log.info("…", { … })\` are\n` +
|
|
40
|
+
`\t// auto-tagged with this request's { method, path, operationId } (ADR 0061).\n` +
|
|
41
|
+
`\t// Return \`{ status?, headers?, body? }\` (or nothing → 204). Replace the two lines below.\n` +
|
|
42
|
+
`\tapp.log.warn("operation not implemented", {\n` +
|
|
43
|
+
`\t\toperationId: ${key},\n` +
|
|
44
|
+
`\t\tmethod: input.req.method,\n` +
|
|
45
|
+
`\t});\n` +
|
|
46
|
+
`\tthrow new NotImplemented(${key});\n` +
|
|
42
47
|
`});\n`);
|
|
43
48
|
}
|
|
44
49
|
/**
|
|
@@ -38,21 +38,26 @@ export function renderWorkerStub(taskType, typedIn, typedOut) {
|
|
|
38
38
|
else if (typedOut)
|
|
39
39
|
generic = `<Record<string, unknown>, WorkerOutputs[${key}]>`;
|
|
40
40
|
const varsNote = typedIn
|
|
41
|
-
?
|
|
42
|
-
:
|
|
41
|
+
? `\t// \`job.variables\` is typed as WorkerInputs[${key}].`
|
|
42
|
+
: `\t// \`job.variables\` is an open Record<string, unknown> (no declared input type).`;
|
|
43
|
+
// The stub arrives lint-clean under the scaffold's own Biome config: tab-indented, and
|
|
44
|
+
// referencing both `job` and `app` (via the warn line) so `noUnusedFunctionParameters`
|
|
45
|
+
// does not fire before the author has filled in the body.
|
|
43
46
|
return (`// Handler stub for the \`${taskType}\` service task, scaffolded from the model (ADR 0056).\n` +
|
|
44
47
|
`// This file is yours to edit — \`urban gen\` will never overwrite it. Implement the body\n` +
|
|
45
|
-
`// below (use \`app.data.table(...)\` for state)
|
|
48
|
+
`// below (use \`app.data.table(...)\` for state), replacing the guard warn + throw so the\n` +
|
|
49
|
+
`// "worker not implemented" warning does not survive into a working handler.\n` +
|
|
46
50
|
`import type { AppJobHandler } from "@nanobpm/urban/worker";\n` +
|
|
47
51
|
generatedImport +
|
|
48
52
|
`\n` +
|
|
49
53
|
`const handler: AppJobHandler${generic} = async (job, app) => {\n` +
|
|
50
54
|
varsNote +
|
|
51
55
|
`\n` +
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
`\t// Reach app state and services through the injected \`app\` API.\n` +
|
|
57
|
+
`\t// Structured logs: \`app.log.info("done", { … })\` — auto-tagged with this job's\n` +
|
|
58
|
+
`\t// { jobKey, jobType, processInstanceKey, elementId } (ADR 0061).\n` +
|
|
59
|
+
`\tapp.log.warn("worker not implemented", { jobKey: job.jobKey });\n` +
|
|
60
|
+
`\tthrow new Error(${JSON.stringify(`worker not implemented: ${taskType}`)});\n` +
|
|
56
61
|
`};\n` +
|
|
57
62
|
`\n` +
|
|
58
63
|
`export default handler;\n`);
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { GenIO } from "./gen.ts";
|
|
2
2
|
import { type SkippedWorker, type StubManifestEntry } from "./scaffold/workers.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Detect the indentation of a JSON document from its first indented line, so a rewrite can
|
|
5
|
+
* reuse it (`"\t"` or an N-space string) instead of hard-coding one style. Falls back to a tab
|
|
6
|
+
* — the scaffold's Biome default — when the file is empty/single-line or uses no indentation.
|
|
7
|
+
*
|
|
8
|
+
* Any indent — spaces or tabs — is clamped to 10 characters because `JSON.stringify` silently
|
|
9
|
+
* truncates a string `space` argument to 10; returning a wider prefix would promise a fidelity the
|
|
10
|
+
* serializer cannot honor (e.g. an 11+-tab manifest would be rewritten at a different width than
|
|
11
|
+
* its input). Tab indentation (the scaffold default) and any realistic width (2/4 spaces) are
|
|
12
|
+
* unaffected.
|
|
13
|
+
*/
|
|
14
|
+
export declare function detectJsonIndent(json: string): string;
|
|
3
15
|
export interface ScaffoldOptions {
|
|
4
16
|
root: string;
|
|
5
17
|
io: GenIO;
|
package/dist/toolkit/scaffold.js
CHANGED
|
@@ -6,13 +6,38 @@ import { joinPath, readModels } from "./gen.js";
|
|
|
6
6
|
import { planWorkerScaffold, } from "./scaffold/workers.js";
|
|
7
7
|
import { planOperationScaffold } from "./scaffold/operations.js";
|
|
8
8
|
import { parseSpec } from "../openapi/spec.js";
|
|
9
|
+
/**
|
|
10
|
+
* Detect the indentation of a JSON document from its first indented line, so a rewrite can
|
|
11
|
+
* reuse it (`"\t"` or an N-space string) instead of hard-coding one style. Falls back to a tab
|
|
12
|
+
* — the scaffold's Biome default — when the file is empty/single-line or uses no indentation.
|
|
13
|
+
*
|
|
14
|
+
* Any indent — spaces or tabs — is clamped to 10 characters because `JSON.stringify` silently
|
|
15
|
+
* truncates a string `space` argument to 10; returning a wider prefix would promise a fidelity the
|
|
16
|
+
* serializer cannot honor (e.g. an 11+-tab manifest would be rewritten at a different width than
|
|
17
|
+
* its input). Tab indentation (the scaffold default) and any realistic width (2/4 spaces) are
|
|
18
|
+
* unaffected.
|
|
19
|
+
*/
|
|
20
|
+
export function detectJsonIndent(json) {
|
|
21
|
+
// Anchor on any line terminator, not just `\n`, so the width is detected regardless of the
|
|
22
|
+
// manifest's line endings: LF (`\n`), CRLF (`\r\n`, where `\n` already precedes the indent),
|
|
23
|
+
// bare-CR old-Mac (`\r`), and LFCR (`\n\r`) all resolve to the indentation that follows.
|
|
24
|
+
const m = json.match(/[\r\n]([ \t]+)\S/);
|
|
25
|
+
if (!m)
|
|
26
|
+
return "\t";
|
|
27
|
+
const indent = m[1];
|
|
28
|
+
return indent.length > 10 ? indent.slice(0, 10) : indent;
|
|
29
|
+
}
|
|
9
30
|
/** Scaffold write-once worker stubs from the model, wiring them into `manifest.workers[]`. */
|
|
10
31
|
export async function scaffoldWorkers(opts) {
|
|
11
32
|
const { root, io } = opts;
|
|
12
33
|
const write = opts.write ?? false;
|
|
13
34
|
const manifestFile = opts.manifestFile ?? "nano.app.json";
|
|
14
35
|
const manifestPath = joinPath(root, manifestFile);
|
|
15
|
-
const
|
|
36
|
+
const manifestRaw = await io.readText(manifestPath);
|
|
37
|
+
const manifest = JSON.parse(manifestRaw);
|
|
38
|
+
// Preserve the manifest's own indentation when we patch it back (default tab, matching the
|
|
39
|
+
// scaffold's Biome config), so wiring a worker never reformats the file out from under lint.
|
|
40
|
+
const manifestIndent = detectJsonIndent(manifestRaw);
|
|
16
41
|
const models = await readModels(root, io, manifest);
|
|
17
42
|
const declaredTypeIds = Object.keys(manifest.types ?? {});
|
|
18
43
|
const { plans, skipped } = planWorkerScaffold(models, manifest.workers ?? [], declaredTypeIds, manifest.externalTaskTypes ?? []);
|
|
@@ -49,7 +74,7 @@ export async function scaffoldWorkers(opts) {
|
|
|
49
74
|
...manifest,
|
|
50
75
|
workers: [...(manifest.workers ?? []), ...wired],
|
|
51
76
|
};
|
|
52
|
-
await io.writeText(manifestPath, `${JSON.stringify(withWorkers, null,
|
|
77
|
+
await io.writeText(manifestPath, `${JSON.stringify(withWorkers, null, manifestIndent)}\n`);
|
|
53
78
|
manifestPatched = true;
|
|
54
79
|
}
|
|
55
80
|
return { outcomes, skipped, wired, manifestPatched, write };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.78.0",
|
|
4
4
|
"description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"@nanobpm/nano-sdk": "^1.2.5",
|
|
104
104
|
"@nanobpm/workflow": "^0.14.0",
|
|
105
105
|
"bpmn-auto-layout": "^2.0.0-alpha.2",
|
|
106
|
-
"create-urban-app": "^0.15.
|
|
106
|
+
"create-urban-app": "^0.15.2",
|
|
107
107
|
"yaml": "^2.9.0"
|
|
108
108
|
},
|
|
109
109
|
"devDependencies": {
|
|
@@ -11,6 +11,7 @@ import { runInJobContext } from "../execContext.ts";
|
|
|
11
11
|
import type { JobExecContext } from "../execContext.ts";
|
|
12
12
|
import { readLineage } from "../lineage.ts";
|
|
13
13
|
import { LineageStore } from "./lineage-store.ts";
|
|
14
|
+
import { readModelRegistry } from "../read-model.ts";
|
|
14
15
|
import { defaultScheduler, schedulerClock, type SchedulerDeps } from "./scheduler.ts";
|
|
15
16
|
|
|
16
17
|
/** The subset of the engine SDK the LLM decision-rails need: evaluate a DMN decision,
|
|
@@ -135,6 +136,28 @@ function tryLineageStore(app: AppApi, log: RuntimeContext["host"]["log"]): Linea
|
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Apply every managed read-model VIEW registered in the process-wide `readModelRegistry` to the app's
|
|
141
|
+
* DEFAULT data source (ADR 0065, surface #3). A `defineReadModel(...)` declaration compiles to a
|
|
142
|
+
* managed SQLite VIEW; provisioning it here — the same per-source boot path `tryLineageStore` uses —
|
|
143
|
+
* means an app (or a canonical-projection sidecar) that registers a read model gets its VIEW created
|
|
144
|
+
* for free, with no hand-written migration. The registry is empty until a sibling task registers into
|
|
145
|
+
* it, so this is a silent no-op in the base framework. Absent- and error-safe like lineage: an app
|
|
146
|
+
* with no datasource provisions nothing, and a VIEW-provisioning failure never breaks a worker mount.
|
|
147
|
+
*/
|
|
148
|
+
function tryProvisionReadModelViews(app: AppApi, log: RuntimeContext["host"]["log"]): void {
|
|
149
|
+
if (readModelRegistry.all().length === 0) return;
|
|
150
|
+
if (!app.data.hasDefaultSource()) {
|
|
151
|
+
log("debug", "read-models: no default data source; managed VIEW provisioning skipped");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
readModelRegistry.ensureViews(app.data.source().db);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
log("warn", "read-models: failed to provision managed VIEWs", { error: String(err) });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
138
161
|
/** Record an activated job's lineage edge, never letting a projection write break the job. */
|
|
139
162
|
function recordJobLineage(store: LineageStore, job: EngineJob, log: RuntimeContext["host"]["log"]): void {
|
|
140
163
|
try {
|
|
@@ -192,6 +215,11 @@ export async function mountWorkers(
|
|
|
192
215
|
// app configures no default data source.
|
|
193
216
|
const lineageStore = tryLineageStore(app, ctx.host.log);
|
|
194
217
|
|
|
218
|
+
// The framework read-model VIEWs (ADR 0065): apply every registered managed VIEW to the app's
|
|
219
|
+
// default data source, the same per-source boot path lineage uses. A no-op until a sibling task
|
|
220
|
+
// registers a read model into `readModelRegistry`.
|
|
221
|
+
tryProvisionReadModelViews(app, ctx.host.log);
|
|
222
|
+
|
|
195
223
|
// Cache module loads so a multi-type handler module is imported once.
|
|
196
224
|
const moduleCache = new Map<string, Record<string, unknown>>();
|
|
197
225
|
const joinRoot = (p: string): string =>
|