@intentius/chant 0.109.0 → 0.111.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/cli/main.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +30 -2
- package/dist/discovery/param-deps.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +9 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/fold-provenance.d.ts +14 -2
- package/dist/fold-provenance.d.ts.map +1 -1
- package/dist/import/ir-utils.d.ts.map +1 -1
- package/dist/import/parser.d.ts +8 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/op/activities/factory.d.ts.map +1 -1
- package/dist/provenance.d.ts +11 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/serializer-walker.d.ts.map +1 -1
- package/dist/stack-output.d.ts +5 -0
- package/dist/stack-output.d.ts.map +1 -1
- package/dist/workspace/box-cli.d.ts +3 -2
- package/dist/workspace/box-cli.d.ts.map +1 -1
- package/dist/workspace/box-factory-write.d.ts +71 -0
- package/dist/workspace/box-factory-write.d.ts.map +1 -0
- package/dist/workspace/box-intent.d.ts +6 -0
- package/dist/workspace/box-intent.d.ts.map +1 -1
- package/dist/workspace/declaration-write-cli.d.ts +11 -0
- package/dist/workspace/declaration-write-cli.d.ts.map +1 -0
- package/dist/workspace/declaration-write.d.ts +78 -0
- package/dist/workspace/declaration-write.d.ts.map +1 -0
- package/dist/workspace/declaration.d.ts +8 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/json-edit.d.ts +11 -0
- package/dist/workspace/json-edit.d.ts.map +1 -1
- package/dist/workspace/ls.d.ts +8 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +4 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/write-scope.d.ts +10 -0
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/import.test.ts +5 -4
- package/src/cli/main.test.ts +2 -0
- package/src/cli/main.ts +18 -0
- package/src/discovery/fold-composite.test.ts +22 -0
- package/src/discovery/fold-host-composite.test.ts +23 -0
- package/src/discovery/fold-import.ts +73 -21
- package/src/discovery/fold-project-provenance.test.ts +52 -0
- package/src/discovery/param-deps.test.ts +43 -2
- package/src/discovery/param-deps.ts +101 -2
- package/src/fold/subset.ts +9 -2
- package/src/fold-provenance.test.ts +11 -0
- package/src/fold-provenance.ts +22 -2
- package/src/import/ir-utils.ts +6 -1
- package/src/import/parser.ts +8 -0
- package/src/op/activities/factory.ts +3 -1
- package/src/op/factory.e2e.test.ts +3 -1
- package/src/provenance.ts +13 -1
- package/src/serializer-walker.test.ts +10 -0
- package/src/serializer-walker.ts +17 -1
- package/src/stack-output.ts +6 -1
- package/src/workspace/box-cli.ts +35 -4
- package/src/workspace/box-factory-write.schema.json +342 -0
- package/src/workspace/box-factory-write.test.ts +108 -0
- package/src/workspace/box-factory-write.ts +226 -0
- package/src/workspace/box-intent.test.ts +14 -4
- package/src/workspace/box-intent.ts +9 -1
- package/src/workspace/declaration-extensions.test.ts +79 -0
- package/src/workspace/declaration-write-cli.ts +82 -0
- package/src/workspace/declaration-write.test.ts +155 -0
- package/src/workspace/declaration-write.ts +275 -0
- package/src/workspace/declaration.ts +19 -1
- package/src/workspace/json-edit.ts +71 -0
- package/src/workspace/ls-contract.test.ts +53 -0
- package/src/workspace/ls.schema.json +11 -0
- package/src/workspace/ls.ts +18 -0
- package/src/workspace/member-write.schema.json +193 -0
- package/src/workspace/reason-codes.test.ts +4 -0
- package/src/workspace/reason-codes.ts +6 -0
- package/src/workspace/status.schema.json +10 -1
- package/src/workspace/status.ts +25 -6
- package/src/workspace/write-scope.ts +17 -0
- package/src/yaml.test.ts +6 -0
- package/src/yaml.ts +4 -0
|
@@ -530,3 +530,55 @@ describe("a member destructured from a called chant composite is unknown (chant#
|
|
|
530
530
|
expectAllUnknown(built.foldProvenance, ["namespace", "resourceQuota", "sharedNamespace", "sharedResourceQuota"]);
|
|
531
531
|
});
|
|
532
532
|
});
|
|
533
|
+
|
|
534
|
+
describe("a value host code produced is unknown, never direct (typescript-as-data#248)", () => {
|
|
535
|
+
let testDir: string;
|
|
536
|
+
let srcDir: string;
|
|
537
|
+
let file: string;
|
|
538
|
+
|
|
539
|
+
beforeAll(async () => {
|
|
540
|
+
const dir = join(repoRoot, ".cache", `chant-tsad-248-${process.pid}`);
|
|
541
|
+
await rm(dir, { recursive: true, force: true });
|
|
542
|
+
await mkdir(join(dir, "src"), { recursive: true });
|
|
543
|
+
testDir = await realpath(dir);
|
|
544
|
+
srcDir = join(testDir, "src");
|
|
545
|
+
file = join(srcDir, "main.ts");
|
|
546
|
+
await writeFile(
|
|
547
|
+
file,
|
|
548
|
+
`
|
|
549
|
+
import { Bucket, AWS } from ${JSON.stringify(LEXICON)};
|
|
550
|
+
const upper = "logs".toUpperCase();
|
|
551
|
+
export const bucket = new Bucket({
|
|
552
|
+
BucketName: "x".toUpperCase(),
|
|
553
|
+
Tags: [{ Key: "n", Value: [1, 2].join("-") }],
|
|
554
|
+
ObjectLockEnabled: true,
|
|
555
|
+
});
|
|
556
|
+
export const logs = new Bucket({ BucketName: upper, AccelerateConfiguration: { AccelerationStatus: AWS.Region } });
|
|
557
|
+
`,
|
|
558
|
+
);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
afterAll(async () => {
|
|
562
|
+
await rm(testDir, { recursive: true, force: true });
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("a host call and a host value report unknown with the reference's reasons, as build() does", async () => {
|
|
566
|
+
const verdict = (await foldProject([file], [], { lexicons: [LEXICON_NAME] })).get(file)!;
|
|
567
|
+
expect(verdict.verdict).toBe("fold");
|
|
568
|
+
const fromFold = verdict.foldProvenance!;
|
|
569
|
+
expect(fromFold.bucket.fields).toEqual({
|
|
570
|
+
BucketName: { kind: "unknown", reason: "host-call" },
|
|
571
|
+
ObjectLockEnabled: { kind: "direct" },
|
|
572
|
+
Tags: { kind: "unknown", reason: "host-call" },
|
|
573
|
+
});
|
|
574
|
+
expect(fromFold.logs.fields).toEqual({
|
|
575
|
+
"AccelerateConfiguration.AccelerationStatus": { kind: "unknown", reason: "host-value" },
|
|
576
|
+
BucketName: { kind: "unknown", reason: "host-call" },
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const built = await build(srcDir, [namesSerializer], undefined, { fold: true, lexicons: [LEXICON_NAME] });
|
|
580
|
+
expect(built.errors).toEqual([]);
|
|
581
|
+
expect(built.foldProvenance.bucket).toEqual(fromFold.bucket);
|
|
582
|
+
expect(built.foldProvenance.logs).toEqual(fromFold.logs);
|
|
583
|
+
});
|
|
584
|
+
});
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
collectParamDependencies,
|
|
8
8
|
collectTagOrigins,
|
|
9
9
|
type CompositeCallSite,
|
|
10
|
+
type HostBindings,
|
|
10
11
|
} from "./param-deps";
|
|
11
12
|
import type { PathOrigin } from "../provenance";
|
|
12
13
|
|
|
@@ -14,7 +15,7 @@ import type { PathOrigin } from "../provenance";
|
|
|
14
15
|
* Collect dependencies for the props of the file's single
|
|
15
16
|
* `export const x = new Type({...})`, which is the shape every case here uses.
|
|
16
17
|
*/
|
|
17
|
-
function depsOf(source: string, paramLocals = ["params"]): Record<string, PathOrigin> {
|
|
18
|
+
function depsOf(source: string, paramLocals = ["params"], host?: HostBindings): Record<string, PathOrigin> {
|
|
18
19
|
const file = ts.createSourceFile("fixture.ts", source, ts.ScriptTarget.Latest, true);
|
|
19
20
|
const consts = collectConsts(file);
|
|
20
21
|
let props: ts.ObjectLiteralExpression | undefined;
|
|
@@ -32,7 +33,7 @@ function depsOf(source: string, paramLocals = ["params"]): Record<string, PathOr
|
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
if (!props) throw new Error("fixture has no `const x = new Type({...})`");
|
|
35
|
-
return collectParamDependencies(props, consts, new Set(paramLocals));
|
|
36
|
+
return collectParamDependencies(props, consts, new Set(paramLocals), host);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
const param = (...names: string[]): PathOrigin => ({ kind: "build-param", params: names });
|
|
@@ -411,3 +412,43 @@ describe("argumentLocation (#3597)", () => {
|
|
|
411
412
|
});
|
|
412
413
|
});
|
|
413
414
|
});
|
|
415
|
+
|
|
416
|
+
describe("collectParamDependencies with host bindings (typescript-as-data#248)", () => {
|
|
417
|
+
const host: HostBindings = { values: new Set(["AWS"]), eagerCalls: new Set(["matrix"]) };
|
|
418
|
+
const hostCall: PathOrigin = { kind: "host", reason: "host-call" };
|
|
419
|
+
const hostValue: PathOrigin = { kind: "host", reason: "host-value" };
|
|
420
|
+
|
|
421
|
+
test("a method call on a receiver is a host call, at its own path", () => {
|
|
422
|
+
const source = `export const x = new Thing({ name: "x".toUpperCase(), spec: { tag: [1, 2].join("-") }, fixed: "k" });`;
|
|
423
|
+
expect(depsOf(source, [], host)).toEqual({ name: hostCall, "spec.tag": hostCall });
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("an eager lexicon helper is a host call, and a project function is not", () => {
|
|
427
|
+
const source = `export const x = new Thing({ a: matrix("os"), b: helper("os") });`;
|
|
428
|
+
expect(depsOf(source, [], host)).toEqual({ a: hostCall });
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("a lexicon export is a host value, through a const and an operator", () => {
|
|
432
|
+
const source = [`const region = AWS.Region;`, "export const x = new Thing({ a: `${region}-x`, b: AWS });"].join("\n");
|
|
433
|
+
expect(depsOf(source, [], host)).toEqual({ a: hostValue, b: hostValue });
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("a host read outranks a build parameter the same expression reads", () => {
|
|
437
|
+
const source = `export const x = new Thing({ name: params.name.toUpperCase(), size: params.size });`;
|
|
438
|
+
expect(depsOf(source, ["params"], host)).toEqual({ name: hostCall, size: param("size") });
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("a sibling resource, a member name and a property key are not host reads", () => {
|
|
442
|
+
const source = [
|
|
443
|
+
`const logs = new Bucket({ name: "x".toUpperCase() });`,
|
|
444
|
+
`const AWS2 = 1;`,
|
|
445
|
+
`export const x = new Thing({ a: logs.Arn, b: { AWS: 1 }, c: thing.AWS });`,
|
|
446
|
+
].join("\n");
|
|
447
|
+
expect(depsOf(source, [], host)).toEqual({});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("a tag is not read, its interpolations are", () => {
|
|
451
|
+
const source = "export const x = new Thing({ a: Sub`${AWS.Region}`, b: Sub`plain` });";
|
|
452
|
+
expect(depsOf(source, [], { values: new Set(["AWS", "Sub"]), eagerCalls: new Set() })).toEqual({ a: hostValue });
|
|
453
|
+
});
|
|
454
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from "typescript";
|
|
2
|
-
import type { ArgumentLocation, PathOrigin, SourceLocation } from "../provenance";
|
|
2
|
+
import type { ArgumentLocation, HostOriginReason, PathOrigin, SourceLocation } from "../provenance";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Which build parameters an authored props expression READS, per path (chant
|
|
@@ -117,6 +117,94 @@ function readParams(
|
|
|
117
117
|
visit(expr);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* What a file binds to host code, for {@link hostReadOf} (INTENTIUS/typescript-as-data#248).
|
|
122
|
+
*/
|
|
123
|
+
export interface HostBindings {
|
|
124
|
+
/** Local names bound to a value a lexicon package exports. */
|
|
125
|
+
values: ReadonlySet<string>;
|
|
126
|
+
/** Local names of eager lexicon helpers: a call to one is evaluated by host code at fold time. */
|
|
127
|
+
eagerCalls: ReadonlySet<string>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The first host read in `expr`, following this file's own top-level consts,
|
|
132
|
+
* or `undefined` when it reads none (INTENTIUS/typescript-as-data#248).
|
|
133
|
+
*
|
|
134
|
+
* A method called on a receiver is `host-call` whatever the receiver is, since
|
|
135
|
+
* fold calls the real method on it; so is a call to an eager lexicon helper. An
|
|
136
|
+
* identifier bound to a lexicon export is `host-value`. A call fold reduces to
|
|
137
|
+
* an envelope (an authoring helper, a call-form intrinsic) or evaluates itself
|
|
138
|
+
* (a project function) is not host code, and only its arguments are read.
|
|
139
|
+
*
|
|
140
|
+
* Syntactic, like {@link readParams}, so it over-reports in one place: both
|
|
141
|
+
* arms of a conditional are read although only one is taken. That errs toward
|
|
142
|
+
* `unknown`, which never licenses an edit `direct` would not.
|
|
143
|
+
*/
|
|
144
|
+
export function hostReadOf(
|
|
145
|
+
expr: ts.Expression,
|
|
146
|
+
consts: ReadonlyMap<string, ts.Expression>,
|
|
147
|
+
paramLocals: ReadonlySet<string>,
|
|
148
|
+
host: HostBindings,
|
|
149
|
+
): HostOriginReason | undefined {
|
|
150
|
+
const followed = new Set<string>();
|
|
151
|
+
let found: HostOriginReason | undefined;
|
|
152
|
+
|
|
153
|
+
const visit = (node: ts.Node): void => {
|
|
154
|
+
if (found) return;
|
|
155
|
+
if (ts.isCallExpression(node)) {
|
|
156
|
+
const callee = unwrap(node.expression);
|
|
157
|
+
if (ts.isPropertyAccessExpression(callee) || ts.isElementAccessExpression(callee)) {
|
|
158
|
+
found = "host-call";
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (ts.isIdentifier(callee) && !consts.has(callee.text) && host.eagerCalls.has(callee.text)) {
|
|
162
|
+
found = "host-call";
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const argument of node.arguments) visit(argument);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
170
|
+
// The member name is not a reference.
|
|
171
|
+
visit(node.expression);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (ts.isIdentifier(node)) {
|
|
176
|
+
if (paramLocals.has(node.text)) return;
|
|
177
|
+
const initializer = consts.get(node.text);
|
|
178
|
+
if (initializer) {
|
|
179
|
+
// A sibling resource is a reference to it, not a read of its props.
|
|
180
|
+
if (ts.isNewExpression(unwrap(initializer)) || followed.has(node.text)) return;
|
|
181
|
+
followed.add(node.text);
|
|
182
|
+
visit(initializer);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (host.values.has(node.text)) found = "host-value";
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (ts.isPropertyAssignment(node)) {
|
|
190
|
+
visit(node.initializer);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// A tag is an intrinsic the lexicon registered, and only its
|
|
195
|
+
// interpolations are read.
|
|
196
|
+
if (ts.isTaggedTemplateExpression(node)) {
|
|
197
|
+
visit(node.template);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
ts.forEachChild(node, visit);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
visit(expr);
|
|
205
|
+
return found;
|
|
206
|
+
}
|
|
207
|
+
|
|
120
208
|
/**
|
|
121
209
|
* Path → build-parameter origin for one resource's props object literal.
|
|
122
210
|
*
|
|
@@ -129,16 +217,27 @@ function readParams(
|
|
|
129
217
|
* A spread is attributed to the object it spreads INTO, at that object's own
|
|
130
218
|
* path (the entity root for a top-level spread), since which keys it
|
|
131
219
|
* contributes is not knowable here.
|
|
220
|
+
*
|
|
221
|
+
* With `host`, a path whose expression reads host code ({@link hostReadOf})
|
|
222
|
+
* is recorded `host` instead, and that outranks a build parameter it also
|
|
223
|
+
* reads: the value is what host code returned, whatever the parameter was.
|
|
132
224
|
*/
|
|
133
225
|
export function collectParamDependencies(
|
|
134
226
|
props: ts.ObjectLiteralExpression,
|
|
135
227
|
consts: ReadonlyMap<string, ts.Expression>,
|
|
136
228
|
paramLocals: ReadonlySet<string>,
|
|
229
|
+
host?: HostBindings,
|
|
137
230
|
): Record<string, PathOrigin> {
|
|
138
231
|
const out: Record<string, PathOrigin> = {};
|
|
139
|
-
if (paramLocals.size === 0) return out;
|
|
232
|
+
if (paramLocals.size === 0 && !host) return out;
|
|
140
233
|
|
|
141
234
|
const record = (path: string, expr: ts.Expression): void => {
|
|
235
|
+
if (out[path]?.kind === "host") return;
|
|
236
|
+
const reason = host ? hostReadOf(expr, consts, paramLocals, host) : undefined;
|
|
237
|
+
if (reason) {
|
|
238
|
+
out[path] = { kind: "host", reason };
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
142
241
|
const found = new Set<string>();
|
|
143
242
|
readParams(expr, consts, paramLocals, found);
|
|
144
243
|
if (found.size === 0) return;
|
package/src/fold/subset.ts
CHANGED
|
@@ -179,7 +179,7 @@ import { intrinsicCallFolds, intrinsicCallFoldsEagerly, type IntrinsicDef } from
|
|
|
179
179
|
* `spec/VERSION` in the specification repository carries the same string, and
|
|
180
180
|
* the conformance adapter reads this one to fill its `specVersion` field.
|
|
181
181
|
*
|
|
182
|
-
* ## Why 2.
|
|
182
|
+
* ## Why 2.3, and what each version since 1.0 asked for (chant#2445)
|
|
183
183
|
*
|
|
184
184
|
* Raising this is the last step of adopting a rule set, so each version was
|
|
185
185
|
* checked against what chant does rather than against whether the suite is
|
|
@@ -228,6 +228,13 @@ import { intrinsicCallFolds, intrinsicCallFoldsEagerly, type IntrinsicDef } from
|
|
|
228
228
|
* chant already did: a call-bound local exported by name (`export { x }`)
|
|
229
229
|
* resolves by F-Call as the single declarator does, and a same-file call's
|
|
230
230
|
* result read inside an F-Call argument resolves once per file (#3329).
|
|
231
|
+
* - **2.3** says a value host code produced, written at an export or into a
|
|
232
|
+
* field, has an unknown origin and never a direct one. chant reports
|
|
233
|
+
* `host-call` and `host-value` for resource fields since
|
|
234
|
+
* typescript-as-data#248. 2.3 also lets `F-Host-Composite` interpret a
|
|
235
|
+
* composite an active package publishes as TypeScript source, which chant
|
|
236
|
+
* has done since #3247, and adds fixtures for a call result read inside an
|
|
237
|
+
* argument, which chant passes since #3610.
|
|
231
238
|
*
|
|
232
239
|
* `scripts/check-docs-citations.ts` refuses to run when this constant and the
|
|
233
240
|
* rules it loads disagree, so this and the `@intentius/tsad-conformance`
|
|
@@ -239,7 +246,7 @@ import { intrinsicCallFolds, intrinsicCallFoldsEagerly, type IntrinsicDef } from
|
|
|
239
246
|
* Until chant#2470 the rules came from a commit `docs-check.yml` checked out;
|
|
240
247
|
* now they come from the published package, so bumping it is what moves this.
|
|
241
248
|
*/
|
|
242
|
-
export const SPEC_VERSION = "2.
|
|
249
|
+
export const SPEC_VERSION = "2.3";
|
|
243
250
|
|
|
244
251
|
/** The two EVL rule ids a shape violation can be attributed to. */
|
|
245
252
|
export type SubsetRuleId = "EVL001" | "EVL003";
|
|
@@ -103,6 +103,17 @@ describe("classifyFieldOrigin", () => {
|
|
|
103
103
|
});
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
test("a host origin is unknown with that reason, never a direct declaration (typescript-as-data#248)", () => {
|
|
107
|
+
expect(classifyFieldOrigin({ kind: "host", reason: "host-call" }, authored)).toEqual({
|
|
108
|
+
kind: "unknown",
|
|
109
|
+
reason: "host-call",
|
|
110
|
+
});
|
|
111
|
+
expect(classifyFieldOrigin({ kind: "host", reason: "host-value" }, authored)).toEqual({
|
|
112
|
+
kind: "unknown",
|
|
113
|
+
reason: "host-value",
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
106
117
|
test("no origin on an entity no composite expanded is a direct declaration", () => {
|
|
107
118
|
expect(classifyFieldOrigin(undefined, authored)).toEqual({ kind: "direct" });
|
|
108
119
|
});
|
package/src/fold-provenance.ts
CHANGED
|
@@ -80,7 +80,11 @@ import {
|
|
|
80
80
|
} from "./provenance";
|
|
81
81
|
import type { DeepEntityDrift, PropertyDrift } from "./lifecycle/deep-diff";
|
|
82
82
|
|
|
83
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Why a field's origin could not be determined. The first two are facts about
|
|
85
|
+
* the BUILD; the host reasons are facts about the expression, and match the
|
|
86
|
+
* strings the specification's reference implementation reports.
|
|
87
|
+
*/
|
|
84
88
|
export type UnknownOriginReason =
|
|
85
89
|
/**
|
|
86
90
|
* A composite expanded the entity and this build did not interpret its body,
|
|
@@ -94,7 +98,15 @@ export type UnknownOriginReason =
|
|
|
94
98
|
* composite make it" is answerable. A sandboxed child's entities arrive this
|
|
95
99
|
* way: `discovery/entity-wire-codec.ts` drops build metadata over the wire.
|
|
96
100
|
*/
|
|
97
|
-
| "no-provenance"
|
|
101
|
+
| "no-provenance"
|
|
102
|
+
/**
|
|
103
|
+
* INTENTIUS/typescript-as-data#248: host code computed the value, by a method
|
|
104
|
+
* called on a real receiver (`"x".toUpperCase()`, `[1, 2].join("-")`) or an
|
|
105
|
+
* eager lexicon helper. The file wrote the call, not the value.
|
|
106
|
+
*/
|
|
107
|
+
| "host-call"
|
|
108
|
+
/** A value a lexicon package exports, bound by an import (`AWS.Region`, `S3Actions.GetObject`). */
|
|
109
|
+
| "host-value";
|
|
98
110
|
|
|
99
111
|
/**
|
|
100
112
|
* What produced one emitted property path.
|
|
@@ -231,6 +243,10 @@ export function classifyFieldOrigin(
|
|
|
231
243
|
return provenance.composite
|
|
232
244
|
? { kind: "unknown", reason: "composite-not-interpreted" }
|
|
233
245
|
: { kind: "direct" };
|
|
246
|
+
case "host":
|
|
247
|
+
// INTENTIUS/typescript-as-data#248 — host code produced the value, so
|
|
248
|
+
// editing the declaration edits a call, not the value it returned.
|
|
249
|
+
return { kind: "unknown", reason: origin.reason };
|
|
234
250
|
case "build-param":
|
|
235
251
|
// A build parameter governs the expression the AUTHOR wrote in their own
|
|
236
252
|
// source, so the declaration is still theirs to edit.
|
|
@@ -307,6 +323,10 @@ export function unknownOriginText(reason: UnknownOriginReason): string {
|
|
|
307
323
|
return "expanded by a composite whose factory this build did not interpret";
|
|
308
324
|
case "no-provenance":
|
|
309
325
|
return "this build recorded no provenance for the entity";
|
|
326
|
+
case "host-call":
|
|
327
|
+
return "the value is what a host call returned";
|
|
328
|
+
case "host-value":
|
|
329
|
+
return "the value is one a host package exports";
|
|
310
330
|
}
|
|
311
331
|
}
|
|
312
332
|
|
package/src/import/ir-utils.ts
CHANGED
|
@@ -36,10 +36,15 @@ export function hasIntrinsicInValue(value: unknown, name: string): boolean {
|
|
|
36
36
|
*/
|
|
37
37
|
export function irUsesIntrinsic(ir: TemplateIR, name: string): boolean {
|
|
38
38
|
for (const resource of ir.resources) {
|
|
39
|
-
if (
|
|
39
|
+
if (
|
|
40
|
+
hasIntrinsicInValue(resource.properties, name) ||
|
|
41
|
+
hasIntrinsicInValue(resource.metadata, name) ||
|
|
42
|
+
hasIntrinsicInValue(resource.attributes, name)
|
|
43
|
+
) {
|
|
40
44
|
return true;
|
|
41
45
|
}
|
|
42
46
|
}
|
|
47
|
+
if (hasIntrinsicInValue(ir.metadata, name)) return true;
|
|
43
48
|
for (const condition of ir.conditions ?? []) {
|
|
44
49
|
if (hasIntrinsicInValue(condition.expression, name)) {
|
|
45
50
|
return true;
|
package/src/import/parser.ts
CHANGED
|
@@ -9,6 +9,9 @@ export interface ParameterIR {
|
|
|
9
9
|
readonly description?: string;
|
|
10
10
|
readonly defaultValue?: unknown;
|
|
11
11
|
readonly required?: boolean;
|
|
12
|
+
/** What the value is checked against, under the source format's own keys
|
|
13
|
+
* and as written (CloudFormation `AllowedValues`, `MinLength`, `NoEcho`). */
|
|
14
|
+
readonly constraints?: Readonly<Record<string, unknown>>;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -22,6 +25,11 @@ export interface ResourceIR {
|
|
|
22
25
|
/** Name of the condition gating this resource (CloudFormation `Condition`
|
|
23
26
|
* key, or equivalent), when the source template declares one (#2069). */
|
|
24
27
|
readonly condition?: string;
|
|
28
|
+
/** Resource-level attributes other than the condition, under the source
|
|
29
|
+
* format's own keys (CloudFormation `DependsOn`, `DeletionPolicy`,
|
|
30
|
+
* `UpdateReplacePolicy`, `UpdatePolicy`, `CreationPolicy`). Values are
|
|
31
|
+
* parsed value trees, like {@link properties}. */
|
|
32
|
+
readonly attributes?: Readonly<Record<string, unknown>>;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
/**
|
|
@@ -22,6 +22,7 @@ import { spawn } from "node:child_process";
|
|
|
22
22
|
import { mkdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
23
23
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
24
24
|
import type { FactoryClaim, FactoryContext, FactoryItem } from "../../workspace/factory-rules";
|
|
25
|
+
import { liveRunEnv } from "../run-live";
|
|
25
26
|
|
|
26
27
|
/** The run's work lease, as `workLeaseOutput()` hands it to a step. */
|
|
27
28
|
export interface FactoryLease {
|
|
@@ -263,7 +264,8 @@ interface Ran {
|
|
|
263
264
|
/** Run argv in cwd, without a shell, asynchronously. */
|
|
264
265
|
function run(argv: readonly string[], cwd: string, env: NodeJS.ProcessEnv): Promise<Ran> {
|
|
265
266
|
return new Promise((settle) => {
|
|
266
|
-
|
|
267
|
+
// The run's activity file, as a shell step gets it, so a builder's lines reach its in-flight record (#3637).
|
|
268
|
+
const child = spawn(argv[0], argv.slice(1), { cwd, env: { ...process.env, ...liveRunEnv(), ...env }, stdio: ["ignore", "pipe", "pipe"] });
|
|
267
269
|
let stdout = "";
|
|
268
270
|
let stderr = "";
|
|
269
271
|
child.stdout.on("data", (b: Buffer) => (stdout += b.toString("utf8")));
|
|
@@ -184,7 +184,7 @@ describe("the factory Op's seams for an orchestrator (studio#382)", () => {
|
|
|
184
184
|
// A builder that reports a run and a check that reports per criterion.
|
|
185
185
|
writeFileSync(join(dir, "hooks", "builder2.cjs"), `${BUILDER}\nconsole.log(JSON.stringify({ ok: true, run: { id: "run-7" }, agent: "builder-small", chantAgent: "factory", records: ["contract:C-1"] }));\n`);
|
|
186
186
|
writeFileSync(join(dir, "hooks", "check2.cjs"), `console.log(JSON.stringify({ criteria: { "AC-1": "pass", "AC-2": "fail" }, built: JSON.parse(process.env.FACTORY_BUILD).agent }));`);
|
|
187
|
-
writeFileSync(join(dir, "hooks", "after.cjs"), `require("node:fs").writeFileSync(require("node:path").join(process.env.FACTORY_FAIL_DIR, "after.json"), JSON.stringify({ outcome: process.env.FACTORY_OUTCOME, commit: process.env.FACTORY_COMMIT, reason: process.env.FACTORY_REASON, check: JSON.parse(process.env.FACTORY_CHECK) }));`);
|
|
187
|
+
writeFileSync(join(dir, "hooks", "after.cjs"), `require("node:fs").writeFileSync(require("node:path").join(process.env.FACTORY_FAIL_DIR, "after.json"), JSON.stringify({ outcome: process.env.FACTORY_OUTCOME, commit: process.env.FACTORY_COMMIT, reason: process.env.FACTORY_REASON, check: JSON.parse(process.env.FACTORY_CHECK), activity: process.env.CHANT_RUN_ACTIVITY ?? null }));`);
|
|
188
188
|
// The prepare hook writes W-110 in the checkout and leaves it uncommitted; the others are taken out of the way.
|
|
189
189
|
writeFileSync(
|
|
190
190
|
join(dir, "hooks", "prepare.cjs"),
|
|
@@ -204,6 +204,8 @@ describe("the factory Op's seams for an orchestrator (studio#382)", () => {
|
|
|
204
204
|
expect(result.workLease).toMatchObject({ item: "W-110", outcome: "not_done" });
|
|
205
205
|
const after = JSON.parse(readFileSync(join(dir, "after.json"), "utf-8"));
|
|
206
206
|
expect(after).toMatchObject({ outcome: "not_done", reason: "criteria AC-2 have no passing evidence", check: { built: "builder-small" } });
|
|
207
|
+
// A hook gets the run's activity file, as a shell step does (#3637).
|
|
208
|
+
expect(after.activity).toMatch(/\.activity\.jsonl$/);
|
|
207
209
|
// The attempt was committed with why, and the run kept that commit.
|
|
208
210
|
expect(after.commit).toMatch(/^[0-9a-f]{40}$/);
|
|
209
211
|
// The kept attempt holds the carried item with AC-1 ticked and AC-2 failed.
|
package/src/provenance.ts
CHANGED
|
@@ -87,6 +87,12 @@ export interface EntityProvenance {
|
|
|
87
87
|
* expression for this path reads no parameter at all. The composite fixes
|
|
88
88
|
* this field, and no argument at the call site moves it.
|
|
89
89
|
*
|
|
90
|
+
* - `host` — the authored expression for this path reads a value host code
|
|
91
|
+
* produced (INTENTIUS/typescript-as-data#248): a method called on a real
|
|
92
|
+
* receiver or an eager lexicon helper (`host-call`), or a value a lexicon
|
|
93
|
+
* package exports (`host-value`). The author wrote the call, not the value,
|
|
94
|
+
* so ./fold-provenance.ts answers `unknown` rather than `direct`.
|
|
95
|
+
*
|
|
90
96
|
* The coarse `composite` and the two fine kinds are not alternatives. A build
|
|
91
97
|
* records `composite` when it expanded a composite without interpreting its
|
|
92
98
|
* body (the run path, an inadmissible factory), and that is why
|
|
@@ -108,7 +114,11 @@ export type PathOrigin =
|
|
|
108
114
|
*/
|
|
109
115
|
arguments?: ArgumentLocation[];
|
|
110
116
|
}
|
|
111
|
-
| { kind: "composite-literal"; composite: string }
|
|
117
|
+
| { kind: "composite-literal"; composite: string }
|
|
118
|
+
| { kind: "host"; reason: HostOriginReason };
|
|
119
|
+
|
|
120
|
+
/** Which host code produced a value: a call it ran, or a value its package exports. */
|
|
121
|
+
export type HostOriginReason = "host-call" | "host-value";
|
|
112
122
|
|
|
113
123
|
/**
|
|
114
124
|
* A place in a source file. `file` is absolute, as
|
|
@@ -270,6 +280,8 @@ export function describePathOrigin(origin: PathOrigin): string {
|
|
|
270
280
|
return `composite ${origin.composite} parameter ${origin.parameters.join(", ")}`;
|
|
271
281
|
case "composite-literal":
|
|
272
282
|
return `composite ${origin.composite} literal`;
|
|
283
|
+
case "host":
|
|
284
|
+
return origin.reason;
|
|
273
285
|
}
|
|
274
286
|
}
|
|
275
287
|
|
|
@@ -118,6 +118,16 @@ describe("walkValue", () => {
|
|
|
118
118
|
});
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
test("an __attrRef envelope naming a property reads the attribute that property's AttrRef carries", () => {
|
|
122
|
+
// A fold writes `db.Endpoint_Address` as the property name; the attribute
|
|
123
|
+
// is `Endpoint.Address`.
|
|
124
|
+
const TestDb = createResource("Test::Db", "test", { Endpoint_Address: "Endpoint.Address" });
|
|
125
|
+
const db = new TestDb({}) as unknown as Declarable;
|
|
126
|
+
const names = new Map<Declarable, string>([[db, "MyDB"]]);
|
|
127
|
+
const value = { __attrRef: { entity: "MyDB", attribute: "Endpoint_Address" } };
|
|
128
|
+
expect(walkValue(value, names, mockVisitor)).toEqual({ __getAtt: ["MyDB", "Endpoint.Address"] });
|
|
129
|
+
});
|
|
130
|
+
|
|
121
131
|
test("resolves standalone __attrRef envelope in plain object", () => {
|
|
122
132
|
const names = new Map<Declarable, string>();
|
|
123
133
|
const value = { __attrRef: { entity: "MyBucket", attribute: "DomainName" } };
|
package/src/serializer-walker.ts
CHANGED
|
@@ -21,6 +21,22 @@ export interface SerializerVisitor {
|
|
|
21
21
|
propertyDeclarable(entity: Declarable, walk: (v: unknown) => unknown): unknown;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The attribute an `{__attrRef}` envelope names. A fold writes the envelope
|
|
26
|
+
* with the property it read (`db.Endpoint_Address`), and the property's own
|
|
27
|
+
* AttrRef carries the attribute (`Endpoint.Address`); they differ when the
|
|
28
|
+
* attribute has a dot. The envelope's name is kept when the entity or
|
|
29
|
+
* property is not found.
|
|
30
|
+
*/
|
|
31
|
+
function envelopeAttribute(entityName: string, property: string, entityNames: Map<Declarable, string>): string {
|
|
32
|
+
for (const [entity, name] of entityNames) {
|
|
33
|
+
if (name !== entityName) continue;
|
|
34
|
+
const held = (entity as unknown as Record<string, unknown>)[property];
|
|
35
|
+
return isAttrRefLike(held) ? held.attribute : property;
|
|
36
|
+
}
|
|
37
|
+
return property;
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
/**
|
|
25
41
|
* Recursively walk a value, converting AttrRefs, Intrinsics, Declarables,
|
|
26
42
|
* arrays, and objects using the provided visitor.
|
|
@@ -100,7 +116,7 @@ export function walkValue(
|
|
|
100
116
|
// Handle serialized AttrRef envelopes (produced by AttrRef.toJSON() inside intrinsics)
|
|
101
117
|
if (typeof value === "object" && "__attrRef" in value) {
|
|
102
118
|
const ref = (value as { __attrRef: { entity: string; attribute: string } }).__attrRef;
|
|
103
|
-
return visitor.attrRef(ref.entity, ref.attribute);
|
|
119
|
+
return visitor.attrRef(ref.entity, envelopeAttribute(ref.entity, ref.attribute, entityNames));
|
|
104
120
|
}
|
|
105
121
|
|
|
106
122
|
// Handle objects
|
package/src/stack-output.ts
CHANGED
|
@@ -37,6 +37,10 @@ export interface StackOutput extends Declarable {
|
|
|
37
37
|
* `Output.Condition`) — a condition name, or the condition Declarable
|
|
38
38
|
* itself, resolved to its logical name at serialization (#2068/#2069). */
|
|
39
39
|
readonly condition?: string | Declarable;
|
|
40
|
+
/** The output's key in the template, when it is not the export name. A
|
|
41
|
+
* template may use one key for an output and a parameter or resource; a
|
|
42
|
+
* module cannot export both under that name (#3604). */
|
|
43
|
+
readonly name?: string;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
/** What a wrapping intrinsic's lexicon gets derived from: either a nested
|
|
@@ -112,7 +116,7 @@ export function isStackOutput(value: unknown): value is StackOutput {
|
|
|
112
116
|
*/
|
|
113
117
|
export function stackOutput(
|
|
114
118
|
ref: AttrRef | Intrinsic | string,
|
|
115
|
-
options?: { description?: string; exportName?: string; lexicon?: string; condition?: string | Declarable },
|
|
119
|
+
options?: { description?: string; exportName?: string; lexicon?: string; condition?: string | Declarable; name?: string },
|
|
116
120
|
): StackOutput {
|
|
117
121
|
// Duck-type, not `instanceof` (chant #1137): AttrRef also implements
|
|
118
122
|
// Intrinsic (a global-symbol marker), so `isIntrinsic(ref)` alone already
|
|
@@ -152,6 +156,7 @@ export function stackOutput(
|
|
|
152
156
|
description: options?.description,
|
|
153
157
|
exportName: options?.exportName,
|
|
154
158
|
condition: options?.condition,
|
|
159
|
+
...(options?.name !== undefined ? { name: options.name } : {}),
|
|
155
160
|
};
|
|
156
161
|
|
|
157
162
|
return output;
|
package/src/workspace/box-cli.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* `chant workspace box`: a member's box block. Two verbs:
|
|
3
3
|
*
|
|
4
4
|
* - `box listing set <member>` (#3308) changes the box's listing
|
|
5
|
-
* (`box-listing.ts`)
|
|
6
|
-
*
|
|
5
|
+
* (`box-listing.ts`), and `box factory set <member>` (#3600) its factory,
|
|
6
|
+
* such as where it publishes (`box-factory-write.ts`). A box's other
|
|
7
|
+
* configuration is changed by editing the declaration in a reviewed change.
|
|
7
8
|
* - `box publish <member> <item> | --records` (#3165, ws-088) runs the
|
|
8
9
|
* publisher the box block names and checks what it did (`box-publish.ts`).
|
|
9
10
|
*/
|
|
@@ -12,12 +13,14 @@ import { readFileSync } from "node:fs";
|
|
|
12
13
|
import { resolve } from "node:path";
|
|
13
14
|
import { formatError } from "../cli/format";
|
|
14
15
|
import type { CommandContext } from "../cli/registry";
|
|
16
|
+
import { BOX_FACTORY_CONTRACT_VERSION, BOX_FACTORY_WRITE_SCHEMA_ID, boxFactorySet, type BoxFactoryWriteDocument } from "./box-factory-write";
|
|
15
17
|
import { BOX_LISTING_CONTRACT_VERSION, BOX_LISTING_WRITE_SCHEMA_ID, boxListingSet, type BoxListingWriteDocument } from "./box-listing";
|
|
16
18
|
import { BOX_PUBLISH_CONTRACT_VERSION, BOX_PUBLISH_SCHEMA_ID, boxPublish, type BoxPublishDocument } from "./box-publish";
|
|
17
19
|
import { readerVersion } from "./declaration";
|
|
18
20
|
import { AGENT_ENV } from "./write-scope";
|
|
19
21
|
|
|
20
22
|
const USAGE = "chant workspace box listing set <member> [--from <file|->] [--cover <image> [--cover-path <path>]] [--by <principal>] [--dry-run] [--json]";
|
|
23
|
+
const FACTORY_USAGE = "chant workspace box factory set <member> --from <file|-> [--by <principal>] [--dry-run] [--json]";
|
|
21
24
|
const PUBLISH_USAGE = "chant workspace box publish <member> (<item> | --records) [--by <principal>] [--head <owner/name>] [--dry-run] [--json]";
|
|
22
25
|
|
|
23
26
|
export async function runWorkspaceBox(ctx: CommandContext): Promise<number> {
|
|
@@ -27,11 +30,13 @@ export async function runWorkspaceBox(ctx: CommandContext): Promise<number> {
|
|
|
27
30
|
const [, noun, verb, member, ...extra] = args.positionals ?? [];
|
|
28
31
|
// For publish, the third positional is the member and the fourth the item.
|
|
29
32
|
if (noun === "publish") return runPublish(ctx, verb, member === undefined ? [] : [member, ...extra]);
|
|
33
|
+
if (noun === "factory" && verb === "set") return runFactorySet(ctx, member, extra);
|
|
30
34
|
if (noun !== "listing" || verb !== "set") {
|
|
35
|
+
const verbs = "listing set <member>, factory set <member> or publish <member>";
|
|
31
36
|
console.error(
|
|
32
37
|
formatError({
|
|
33
|
-
message: noun === undefined ?
|
|
34
|
-
hint: `${USAGE}\n${PUBLISH_USAGE}`,
|
|
38
|
+
message: noun === undefined ? `chant workspace box takes ${verbs}` : `chant workspace box takes ${verbs}, not ${[noun, verb].filter(Boolean).join(" ")}`,
|
|
39
|
+
hint: `${USAGE}\n${FACTORY_USAGE}\n${PUBLISH_USAGE}`,
|
|
35
40
|
}),
|
|
36
41
|
);
|
|
37
42
|
return 1;
|
|
@@ -75,6 +80,32 @@ export async function runWorkspaceBox(ctx: CommandContext): Promise<number> {
|
|
|
75
80
|
);
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
/** `box factory set <member> --from <file|->` (#3600). */
|
|
84
|
+
function runFactorySet(ctx: CommandContext, member: string | undefined, extra: string[]): number {
|
|
85
|
+
const { args } = ctx;
|
|
86
|
+
const cwd = process.cwd();
|
|
87
|
+
const print = (doc: BoxFactoryWriteDocument): number => {
|
|
88
|
+
console.log(JSON.stringify(doc, null, 2));
|
|
89
|
+
return "error" in doc ? 1 : 0;
|
|
90
|
+
};
|
|
91
|
+
const fail = (code: "write-usage-invalid" | "write-input-invalid", message: string) =>
|
|
92
|
+
print({ $schema: BOX_FACTORY_WRITE_SCHEMA_ID, contract: BOX_FACTORY_CONTRACT_VERSION, chant: readerVersion(), member: member ?? null, error: { code, message } });
|
|
93
|
+
if (member === undefined) return fail("write-usage-invalid", `box factory set needs the member whose box builds; ${FACTORY_USAGE}`);
|
|
94
|
+
if (extra.length > 0) return fail("write-usage-invalid", `box factory set takes one member, and was also given ${extra.join(" ")}`);
|
|
95
|
+
for (const [flag, v] of [["--set", args.set], ["--kind", args.kind], ["--at", args.at], ["--cover", args.cover]] as const) {
|
|
96
|
+
if (v !== undefined) return fail("write-usage-invalid", `box factory set takes its fields with --from, not ${flag}`);
|
|
97
|
+
}
|
|
98
|
+
let fields: string | undefined;
|
|
99
|
+
if (args.migrateFrom !== undefined) {
|
|
100
|
+
try {
|
|
101
|
+
fields = readFileSync(args.migrateFrom === "-" ? 0 : resolve(cwd, args.migrateFrom), "utf-8");
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return fail("write-input-invalid", `--from ${args.migrateFrom} could not be read: ${err instanceof Error ? err.message : String(err)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return print(boxFactorySet({ cwd, member, fields, by: args.by, agent: process.env[AGENT_ENV] || undefined, dryRun: args.dryRun === true }));
|
|
107
|
+
}
|
|
108
|
+
|
|
78
109
|
/** `box publish <member> <item>` or `box publish <member> --records`. */
|
|
79
110
|
function runPublish(ctx: CommandContext, member: string | undefined, rest: string[]): number {
|
|
80
111
|
const { args } = ctx;
|