@aid-framework/generator-sdk 0.1.39 → 0.1.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +167 -117
  2. package/package.json +3 -3
package/README.md CHANGED
@@ -1,96 +1,129 @@
1
- # `@aid-framework/generator-sdk` — generator plugin interface
1
+ # `@aid-framework/generator-sdk`
2
2
 
3
- Defines what a generator plugin *is*, so third-party and first-party targets are built the same way, and
4
- supplies the three things every generator needs to be safe to re-run: a manifest, an ownership/drift
5
- algorithm, and a determinism contract.
3
+ The plugin contract for [AID Framework](https://github.com/aid-framework/aid-framework) generators. It defines
4
+ what a generator **is**, and supplies the three things every generator needs in order to be safe to re-run: a
5
+ manifest, an ownership and drift algorithm, and a determinism contract.
6
6
 
7
- Layering is strictly one-directional: `spec ← ir ← generator-sdk ← generators ← cli`. This package imports
8
- `@aid-framework/ir` and `@aid-framework/spec`; neither may import this one. That constraint is why a target's capability
9
- declaration travels as **data** (`targetCapabilityDeclarations`) rather than the IR's capability gate
10
- reaching in here — [`@aid-framework/ir`'s README](../ir/README.md) documented the placeholder it ships, and this
11
- package is the replacement for it.
7
+ If you are writing a target — a new language, a new hosting platform — this is the package you implement. If you
8
+ are only *using* AID, you do not call it directly: `aid gen` does.
12
9
 
13
- ## The generator manifest
10
+ ```text
11
+ spec ← ir ← generator-sdk ← generator-<target> ← cli
12
+ ```
14
13
 
15
- A generator declares itself with a `GeneratorManifest` (`GENERATOR_MANIFEST_VERSION` is `"0.1.0"`): `name`,
16
- `target`, `version`, `irRange`, `capabilities`, `emitsAgainst`, `output`, and an optional `optionsSchema`.
14
+ ## Install
17
15
 
18
- `emitsAgainst` is the interesting field, because it is what lets a generated app be reasoned about rather
19
- than only diffed. It names the runtime the output is paired with and the shape of what gets emitted:
16
+ ```sh
17
+ npm install @aid-framework/generator-sdk
18
+ ```
20
19
 
21
- | Key | Values |
22
- | --- | --- |
23
- | `runtime` | e.g. `py-fastapi` |
24
- | `orchestration` | `ORCHESTRATIONS` — Phase 0 emits `plain-async` only |
25
- | `tracingBackend` | `TRACING_BACKENDS` — e.g. `otel-genai` |
26
- | `structuredOutput` | `STRUCTURED_OUTPUTS` — e.g. `pydantic-ai` |
20
+ Requires Node.js 24 or newer.
27
21
 
28
- Validation returns diagnostics, never thrown exceptions, in the same compiler-style shape as `@aid-framework/spec` and
29
- `@aid-framework/ir`, so a user sees one report whatever layer rejected the input. Codes are namespaced `generator/*`
30
- and never reuse a `spec/*` or `ir/*` code.
22
+ ## The three jobs
31
23
 
32
- ```ts
33
- import { parseManifestJson, validateGeneratorSet, checkIRSupport } from '@aid-framework/generator-sdk';
34
- import { IR_VERSION } from '@aid-framework/ir';
24
+ 1. **Say what you are.** A manifest declares the target id, the IR range you accept, the capabilities you can
25
+ honour, and the runtime you emit against.
26
+ 2. **Never destroy a human's edit.** Files are split into a generator-owned tier and a developer-owned tier, and
27
+ regeneration refuses to overwrite a generated file whose bytes have changed.
28
+ 3. **Emit the same bytes twice.** Given the same IR, a generator must produce byte-identical output — no
29
+ timestamps, no host names, no absolute paths, no randomness.
35
30
 
36
- const parsed = parseManifestJson(source);
31
+ ## The manifest
32
+
33
+ ```ts
34
+ import type { GeneratorManifestInput } from '@aid-framework/generator-sdk';
35
+
36
+ const input: GeneratorManifestInput = {
37
+ name: 'my-target',
38
+ target: 'my-target',
39
+ version: '0.1.0',
40
+ irRange: '^0.1.0',
41
+ capabilities: ['http-trigger', 'json-schema', 'otel'],
42
+ emitsAgainst: {
43
+ structuredOutput: 'zod',
44
+ tracing: 'otel-genai',
45
+ },
46
+ output: {
47
+ root: 'app',
48
+ generatedDir: 'generated',
49
+ businessDir: 'business',
50
+ },
51
+ };
52
+
53
+ const parsed = parseManifest(input);
37
54
  if (!parsed.ok) throw new Error(JSON.stringify(parsed.diagnostics, null, 2));
38
55
 
39
- checkIRSupport(parsed.manifest, IR_VERSION); // [] when the generator's irRange covers the IR
40
- validateGeneratorSet([parsed.manifest]); // [] when the set is coherent
56
+ checkIRSupport(parsed.manifest, IR_VERSION); // [] when irRange covers this IR
57
+ validateGeneratorSet([parsed.manifest]); // [] when the set is coherent
41
58
  ```
42
59
 
43
- - `parseManifest(value)` / `parseManifestJson(text)` return `{ ok, manifest, diagnostics }`.
44
- - `validateManifest(value)` returns the diagnostics for one manifest; `validateGeneratorSet(manifests)`
45
- additionally rejects two generators claiming the same `target`.
46
- - `supportsIR(manifest, irVersion?)` and `checkIRSupport(...)` compare against `irRange` using this package's
47
- own semver parser, so the range vocabulary is shared with nothing outside the repo.
48
- - `managedRoots(manifest)` turns a manifest into the `PlanValidationOptions` its plans must satisfy.
49
- - `targetCapabilityDeclarations(manifests)` renders `{ target: capabilities[] }` for
50
- `runGates(ir, { targetCapabilities })`.
51
- - `phase0TargetCapabilities` / `phase0Generators` are the committed Phase 0 declarations.
60
+ `emitsAgainst` is what makes two targets comparable. A target emitting Pydantic models and OTel spans is not
61
+ interchangeable with one emitting Zod and a different tracer, and the declaration says so in **data** rather than
62
+ in prose.
52
63
 
53
- Those declarations are deliberately honest rather than aspirational: `streaming` is declared even though the
54
- runtime is request/response only (so the IR vocabulary is exercised end to end), and no `orchestration` is
55
- declared because Phase 0 control flow is plain async.
64
+ | Key | Vocabulary |
65
+ | --- | --- |
66
+ | `runtime` | The runtime the output is paired with, e.g. `py-fastapi` |
67
+ | `orchestration` | `ORCHESTRATIONS` |
68
+ | `tracingBackend` | `TRACING_BACKENDS`, e.g. `otel-genai` |
69
+ | `structuredOutput` | `STRUCTURED_OUTPUTS`, e.g. `zod`, `pydantic` |
56
70
 
57
- ## `generated/` vs `business/` — the ownership model
71
+ Validation returns diagnostics, never thrown exceptions, in the same compiler-style shape as
72
+ `@aid-framework/spec` and `@aid-framework/ir`. Codes are namespaced `generator/*`. `validateGeneratorSet`
73
+ additionally rejects two generators claiming the same `target`.
58
74
 
59
- The three-tier split is the whole reason regeneration is safe (§8.3, §8.4):
75
+ A manifest is data, which is why capabilities travel into the IR gates as an argument:
76
+
77
+ ```ts
78
+ import { targetCapabilityDeclarations } from '@aid-framework/generator-sdk';
79
+ import { runGates } from '@aid-framework/ir';
60
80
 
61
- | Tier | Owner | Regeneration behaviour |
81
+ runGates(ir, { targetCapabilities: targetCapabilityDeclarations(manifests) });
82
+ ```
83
+
84
+ ## `generated/` and `business/`
85
+
86
+ The two-tier split is the reason regeneration is safe.
87
+
88
+ | Tier | Owner | On regeneration |
62
89
  | --- | --- | --- |
63
- | `generated/` | the generator | overwritten on every run — never hand-edit |
64
- | `business/` | the developer | written **once**, never overwritten |
65
- | the seam | — | `business/` is what the generator calls into; it is the escape hatch |
90
+ | `generated/` | the generator | overwritten — never hand-edit |
91
+ | `business/` | you | created once if missing, then **never touched** |
92
+
93
+ `business/` is the seam: it is what generated code calls into. That is where your prompts, tool handlers and
94
+ domain logic live, and it is why regenerating does not cost you your work.
66
95
 
67
- Ownership is decided by **where a path lives**, not by what a generator says about it: every planned path is
68
- normalized through `src/ownership.ts`, so a generator cannot escape its own root (`escapesRoot`,
69
- `isAbsolutePath`), cannot claim a `business/` path as generated (`planOwnerMismatch`), and cannot write the
70
- manifest file (`isReservedPath`). `ownerForPath`, `isGeneratedPath` and `isBusinessPath` are the predicates,
71
- parameterized by `DEFAULT_GENERATED_DIR` / `DEFAULT_BUSINESS_DIR`.
96
+ Ownership is decided by **where a path lives**, not by what a generator claims. Every planned path is normalized,
97
+ so a generator cannot escape its own root, cannot claim a `business/` path as generated, and cannot write the
98
+ manifest file itself.
99
+
100
+ ```ts
101
+ import {
102
+ isBusinessPath, isGeneratedPath, isReservedPath, ownerForPath, validatePlan,
103
+ } from '@aid-framework/generator-sdk';
104
+ ```
72
105
 
73
106
  ## The drift algorithm
74
107
 
75
- This is the algorithm the ownership model exists to support (§10), stated once in `src/drift.ts`:
108
+ This is the whole safety story, in five steps:
76
109
 
77
- 1. Plan the new file set — `plan(ir) -> Plan` is pure and writes nothing.
78
- 2. For each planned `generated/` file: if it exists and its hash is **not** the one recorded in the
79
- manifest, a human edited generated code. Do not overwrite it; write the new content to `<path>.aid-rej`
80
- and fail with guidance.
81
- 3. For each planned `business/` file: create only if missing. Never overwrite.
82
- 4. Write atomically, then update the manifest.
83
- 5. Report created / updated / unchanged / conflicts.
110
+ 1. **Plan.** `plan(ir)` is pure. It writes nothing.
111
+ 2. **Check generated files.** If a planned `generated/` file exists and its hash is not the one recorded in the
112
+ manifest, a human edited it. Do not overwrite. Write the new content beside it as `<path>.aid-rej` and fail
113
+ with guidance.
114
+ 3. **Create business files.** Only if missing. Never overwrite.
115
+ 4. **Write atomically**, then update the manifest.
116
+ 5. **Report** created / updated / unchanged / kept / conflicts.
84
117
 
85
- Step 2 is the whole point. A generator that overwrites `generated/` unconditionally destroys the one signal
86
- it has that a human disagreed with it, and the loss is silent. Refusing, keeping the rejected bytes next to
87
- the file (`.aid-rej`, per `REJECTION_SUFFIX`), and naming both escapes — `aid eject <path>` to take
88
- ownership of a file, `--force` to discard the edits — turns silent data loss into a decision.
118
+ Step 2 is the point. A generator that overwrites unconditionally destroys the one signal it has that a human
119
+ disagreed with it, and the loss is silent. Refusing, keeping the rejected bytes, and naming the escape —
120
+ `--force` to discard the edit — turns silent data loss into a decision you make explicitly.
89
121
 
90
122
  ```ts
91
- import { planRegeneration, regenerate, hasDrift } from '@aid-framework/generator-sdk';
123
+ import { hasDrift, planRegeneration, regenerate } from '@aid-framework/generator-sdk';
92
124
 
93
- const preview = planRegeneration(plan, { sink, manifest }); // pure: nothing is written
125
+ // Pure: computes what would happen and writes nothing.
126
+ const preview = planRegeneration(plan, { sink, manifest });
94
127
  if (hasDrift(preview)) {
95
128
  // preview.actions carries { kind: 'drift', path, recordedSha256, actualSha256 }
96
129
  }
@@ -98,39 +131,20 @@ if (hasDrift(preview)) {
98
131
  const { report, manifest: next } = regenerate(plan, { sink, manifest, provenance });
99
132
  ```
100
133
 
101
- `RegenerationAction` is `create | update | unchanged | keep-business | drift`, so a caller can render a
102
- `--dry-run` from the same code path that writes. Two limits are deliberate: a planned file that is newer than
103
- the manifest but has **no** record at all is treated as drift rather than adopted (an unrecorded file under
104
- `generated/` cannot be proven unmodified), and stale generated files — recorded but no longer planned — are
105
- reported but never deleted unless `prune` is passed explicitly, because deletion is the one operation with no
106
- recovery.
107
-
108
- `createNodeSink(root)` writes to disk; `createMemorySink()` is the same `RegenerationSink` interface in
109
- memory, which is what the drift tests drive every action through.
134
+ `RegenerationAction` is `create | update | unchanged | keep-business | drift`, so `--dry-run` renders from the
135
+ same code path that writes.
110
136
 
111
- ## The manifest on disk
112
-
113
- `.aid-manifest.json` (`DEFAULT_MANIFEST_FILENAME`, version `AID_MANIFEST_VERSION`) records provenance —
114
- generator name and version, IR version, spec version — plus one entry per file: `path`, `owner`, `sha256`,
115
- `generator`, `generatorVersion`. It is what makes step 2 above possible, and what lets a later run tell
116
- `unchanged` from `edited`.
117
-
118
- ```ts
119
- import { manifestFor, parseAidManifestJson, serializeAidManifest } from '@aid-framework/generator-sdk';
137
+ Two limits are deliberate:
120
138
 
121
- const manifest = manifestFor(files, provenance);
122
- const text = serializeAidManifest(manifest);
123
- ```
124
-
125
- `serializeAidManifest` is canonical — keys sorted, LF endings, one trailing newline — so the manifest itself
126
- does not churn a diff. `emptyAidManifest`, `manifestRecordFor`, and
127
- `parseAidManifest`/`parseAidManifestJson` round out the module.
139
+ - A file under `generated/` that exists but has **no** record in the manifest is treated as drift rather than
140
+ adopted, because it cannot be proven unmodified.
141
+ - Stale generated files — recorded but no longer planned — are reported but never deleted unless pruning is
142
+ requested explicitly. Deletion is the one operation with no recovery.
128
143
 
129
144
  ## Determinism
130
145
 
131
- The contract is narrow and absolute (§8.2): **no timestamps, no host names, no absolute paths, no
132
- randomness** in generated code. A single timestamp turns every regeneration into a diff, and the golden-file
133
- tests (§12 regime A) compare bytes.
146
+ The contract is narrow and absolute: **no timestamps, no host names, no absolute paths, no randomness** in
147
+ generated code. One timestamp turns every regeneration into a diff, and golden-file tests compare bytes.
134
148
 
135
149
  ```ts
136
150
  import { checkDeterminism } from '@aid-framework/generator-sdk';
@@ -138,34 +152,70 @@ import { checkDeterminism } from '@aid-framework/generator-sdk';
138
152
  const violations = checkDeterminism(files); // { path, pattern, line, excerpt }[]
139
153
  ```
140
154
 
141
- - Primitives: `normalizeNewlines`, `ensureTrailingNewline`, `normalizeText`, `sha256Hex`,
142
- `stableId(namespace, value)`, and `compareIds` / `sortById` / `sortByPath` / `stableSortBy` for ordering by
143
- a **stable ID** rather than by whatever order the IR happened to be walked in.
144
- - `generatedBanner(options)` writes the do-not-edit header (`GENERATED_BANNER_MARKER`), which is the only
145
- thing standing between a reader and an edit the next regeneration will refuse.
146
- - `DETERMINISM_PATTERNS` is the rule set and `scanForNonDeterminism(path, content)` the per-file checker, so
147
- a generator's own tests can run the emitted set through it and fail the build. That is what makes the
148
- contract testable instead of aspirational.
155
+ | Export | Purpose |
156
+ | --- | --- |
157
+ | `normalizeNewlines`, `ensureTrailingNewline`, `normalizeText` | Line-ending and trailing-newline normalization |
158
+ | `sha256Hex`, `stableId` | Content hashes and stable identities |
159
+ | `compareIds`, `sortById`, `sortByPath`, `stableSortBy` | Order by a stable id, not by walk order |
160
+ | `generatedBanner` | The do-not-edit header and its marker |
161
+ | `DETERMINISM_PATTERNS`, `scanForNonDeterminism` | The rule set and a per-file checker |
162
+ | `checkDeterminism` | Run the whole rule set over a file set |
149
163
 
150
- The contract applies to **generated code, never to model output** (§8.2): completions are expected to vary,
151
- and eval gating accounts for that by sampling rather than by comparing bytes.
164
+ `scanForNonDeterminism` is what lets a generator's own tests fail the build when it emits a timestamp. That is
165
+ what makes the contract testable instead of aspirational.
152
166
 
153
- ## Development
167
+ The contract applies to **generated code, never to model output**. Completions are expected to vary; eval gating
168
+ accounts for that by sampling rather than by comparing bytes.
154
169
 
155
- ```sh
156
- pnpm build
157
- pnpm test
170
+ ## The manifest on disk
171
+
172
+ `aid.manifest.json` records provenance — generator name and version, IR version, spec version — plus one entry
173
+ per file: `path`, `owner`, `sha256`, `generator`, `generatorVersion`. That record is what makes step 2 above
174
+ possible.
175
+
176
+ ```ts
177
+ import { manifestFor, parseAidManifestJson, serializeAidManifest } from '@aid-framework/generator-sdk';
178
+
179
+ const written = manifestFor(files, provenance);
180
+ const text = serializeAidManifest(written); // canonical: sorted keys, LF, one trailing newline
158
181
  ```
159
182
 
160
- `test/` covers the manifest and plan validation, the ownership predicates, the determinism primitives, the
161
- semver range parser, the on-disk manifest, both sinks, and — the suite that matters most — `drift.test.ts`,
162
- which drives every `RegenerationAction` including the refuse-and-write-rejection path.
183
+ ## The generator interface
163
184
 
164
- **Not responsible for:** emitting any specific target's code (see [`../generators/`](../generators/)) or for
165
- the behavior of generated apps (see [`../../runtimes/`](../../runtimes/)).
185
+ ```ts
186
+ import type { Generator } from '@aid-framework/generator-sdk';
187
+
188
+ export const myGenerator: Generator = {
189
+ name: manifest.name,
190
+ target: manifest.target,
191
+ version: manifest.version,
192
+ irRange: manifest.irRange,
193
+ plan: (ir) => planMyTarget(ir),
194
+ emit: (plan) => plan.files,
195
+ };
196
+ ```
166
197
 
167
- **Status:** the three responsibilities above are implemented for Phase 0. Deferred by design: escalation
168
- routing and any orchestration beyond plain async (§14) — which is why the Phase 0 capability declarations
169
- declare none.
198
+ Every identity field is read off the manifest, so a manifest a loader validated and the generator it then ran
199
+ cannot disagree about which target, version or IR range they describe.
170
200
 
171
- Reference: [`docs/design.md` §8, §8.3, §8.4, §10](../../docs/design.md).
201
+ ## API
202
+
203
+ | Export | Purpose |
204
+ | --- | --- |
205
+ | `parseManifest`, `parseManifestJson`, `validateManifest`, `validateGeneratorSet` | Manifest handling |
206
+ | `checkIRSupport`, `supportsIR` | IR range compatibility |
207
+ | `managedRoots`, `targetCapabilityDeclarations` | Manifest → IR gates |
208
+ | `validatePlan`, `isRecord` | Plan validation |
209
+ | `planRegeneration`, `regenerate`, `checkDrift`, `hasDrift` | The drift algorithm |
210
+ | `staleGeneratedFiles`, `generatedFiles` | Inventory of a previous run |
211
+ | `createNodeSink`, `createMemorySink` | On-disk and in-memory regeneration sinks |
212
+ | `ownerForPath`, `isGeneratedPath`, `isBusinessPath`, `isReservedPath`, `escapesRoot` | Ownership predicates |
213
+ | `rejectionPath` | Where rejected bytes are parked |
214
+ | `manifestFor`, `parseAidManifest`, `serializeAidManifest` | The on-disk manifest |
215
+ | `checkDeterminism`, `DETERMINISM_PATTERNS`, `scanForNonDeterminism` | Determinism |
216
+ | `parseRange`, `satisfies`, `compareVersions` | The shared semver vocabulary |
217
+
218
+ ## License
219
+
220
+ [Apache-2.0](https://github.com/aid-framework/aid-framework/blob/main/LICENSE). Code generated by AID Framework is
221
+ not subject to this license.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aid-framework/generator-sdk",
3
- "version": "0.1.39",
3
+ "version": "0.1.48",
4
4
  "description": "The AID Framework generator plugin interface: the manifest contract, the drift/ownership algorithm, and the determinism helpers every target generator emits through.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -30,8 +30,8 @@
30
30
  "access": "public"
31
31
  },
32
32
  "dependencies": {
33
- "@aid-framework/ir": "0.1.39",
34
- "@aid-framework/spec": "0.1.39"
33
+ "@aid-framework/ir": "0.1.48",
34
+ "@aid-framework/spec": "0.1.48"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^26.6.5"