@aid-framework/generator-sdk 0.1.39 → 0.1.51
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 +167 -117
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,96 +1,129 @@
|
|
|
1
|
-
# `@aid-framework/generator-sdk`
|
|
1
|
+
# `@aid-framework/generator-sdk`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
supplies the three things every generator needs to be safe to re-run: a
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
10
|
+
```text
|
|
11
|
+
spec ← ir ← generator-sdk ← generator-<target> ← cli
|
|
12
|
+
```
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
`target`, `version`, `irRange`, `capabilities`, `emitsAgainst`, `output`, and an optional `optionsSchema`.
|
|
14
|
+
## Install
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
```sh
|
|
17
|
+
npm install @aid-framework/generator-sdk
|
|
18
|
+
```
|
|
20
19
|
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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);
|
|
40
|
-
validateGeneratorSet([parsed.manifest]);
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
64
|
-
| `business/` |
|
|
65
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
108
|
+
This is the whole safety story, in five steps:
|
|
76
109
|
|
|
77
|
-
1. Plan
|
|
78
|
-
2.
|
|
79
|
-
manifest, a human edited
|
|
80
|
-
|
|
81
|
-
3.
|
|
82
|
-
4. Write atomically
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
123
|
+
import { hasDrift, planRegeneration, regenerate } from '@aid-framework/generator-sdk';
|
|
92
124
|
|
|
93
|
-
|
|
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
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
|
132
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.51",
|
|
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.
|
|
34
|
-
"@aid-framework/spec": "0.1.
|
|
33
|
+
"@aid-framework/ir": "0.1.51",
|
|
34
|
+
"@aid-framework/spec": "0.1.51"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^26.6.5"
|