@intentius/chant 0.37.0 → 0.38.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/commands/check-lexicon-mcp.d.ts +44 -0
- package/dist/cli/commands/check-lexicon-mcp.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon-plugin.d.ts +57 -0
- package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/handlers/emulator.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +26 -2
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/codegen/docs-sidebar.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts +11 -0
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/lexicon.d.ts +66 -37
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/live-endpoint.d.ts +21 -22
- package/dist/live-endpoint.d.ts.map +1 -1
- package/dist/op/emulator-freshness.d.ts +44 -0
- package/dist/op/emulator-freshness.d.ts.map +1 -0
- package/dist/op/emulator-lifecycle.d.ts +36 -0
- package/dist/op/emulator-lifecycle.d.ts.map +1 -1
- package/dist/op/index.d.ts +4 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/ownership.d.ts +33 -0
- package/dist/ownership.d.ts.map +1 -1
- package/dist/serializer.d.ts +15 -0
- package/dist/serializer.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audit/catalog.test.ts +58 -6
- package/src/cli/commands/check-lexicon-doc-drift.test.ts +73 -0
- package/src/cli/commands/check-lexicon-mcp.test.ts +93 -0
- package/src/cli/commands/check-lexicon-mcp.ts +103 -0
- package/src/cli/commands/check-lexicon-plugin.test.ts +149 -0
- package/src/cli/commands/check-lexicon-plugin.ts +115 -0
- package/src/cli/commands/check-lexicon.ts +157 -26
- package/src/cli/handlers/components.test.ts +17 -0
- package/src/cli/handlers/components.ts +1 -1
- package/src/cli/handlers/emulator.ts +12 -8
- package/src/cli/handlers/graph.test.ts +71 -12
- package/src/cli/handlers/graph.ts +46 -5
- package/src/cli/handlers/lifecycle.test.ts +25 -4
- package/src/cli/handlers/lifecycle.ts +9 -3
- package/src/cli/mcp/server.test.ts +82 -0
- package/src/cli/mcp/server.ts +40 -5
- package/src/codegen/docs-rule-scanning.ts +12 -3
- package/src/codegen/docs-sections.ts +5 -3
- package/src/codegen/docs-sidebar.ts +8 -2
- package/src/codegen/docs.ts +19 -0
- package/src/lexicon-doc-coverage.test.ts +128 -0
- package/src/lexicon-seams.test.ts +113 -0
- package/src/lexicon.ts +68 -38
- package/src/live-endpoint.test.ts +51 -12
- package/src/live-endpoint.ts +32 -33
- package/src/op/emulator-declaration.test.ts +63 -0
- package/src/op/emulator-freshness.test.ts +135 -0
- package/src/op/emulator-freshness.ts +102 -0
- package/src/op/emulator-lifecycle.ts +49 -0
- package/src/op/index.ts +4 -2
- package/src/ownership.ts +41 -0
- package/src/serializer.ts +16 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generalized from `lexicons/fly/src/emulator-freshness.test.ts` (#1345), which
|
|
3
|
+
* covered fly's two pins. The check now reads every lexicon's declared spec.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect } from "vitest";
|
|
7
|
+
import { compare, parseVersion, latestRelease, checkFreshness, formatResult, unpinned } from "./emulator-freshness";
|
|
8
|
+
import type { EmulatorSpec } from "./emulator-lifecycle";
|
|
9
|
+
|
|
10
|
+
const spec = (over: Partial<EmulatorSpec> = {}): EmulatorSpec => ({
|
|
11
|
+
name: "chant-x",
|
|
12
|
+
image: "org/x:1.0.0",
|
|
13
|
+
containerPort: 1,
|
|
14
|
+
healthPath: "/h",
|
|
15
|
+
...over,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("parseVersion", () => {
|
|
19
|
+
test("reads the tag off an image ref", () => {
|
|
20
|
+
expect(parseVersion("ghcr.io/intentius/mudflaps:0.4.1")).toBe("0.4.1");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("strips a leading v", () => {
|
|
24
|
+
expect(parseVersion("floci/floci:v1.5.34")).toBe("1.5.34");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("a registry port is not a tag", () => {
|
|
28
|
+
// `localhost:5000/floci` has a colon before the last slash.
|
|
29
|
+
expect(parseVersion("localhost:5000/floci")).toBe("");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("an untagged ref has no version", () => {
|
|
33
|
+
expect(parseVersion("floci/floci")).toBe("");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("compare", () => {
|
|
38
|
+
test("behind when latest is newer", () => {
|
|
39
|
+
expect(compare("x", "0.4.1", "0.5.0").behind).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("current when equal", () => {
|
|
43
|
+
expect(compare("x", "1.5.34", "1.5.34").behind).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("not behind when the pin is ahead of the latest release", () => {
|
|
47
|
+
expect(compare("x", "2.0.0", "1.9.9").behind).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("compares numerically, not lexically", () => {
|
|
51
|
+
// "10" < "9" as strings; the whole point of the parse.
|
|
52
|
+
expect(compare("x", "1.9.0", "1.10.0").behind).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("a shorter version is padded with zeros", () => {
|
|
56
|
+
expect(compare("x", "1.5", "1.5.1").behind).toBe(true);
|
|
57
|
+
expect(compare("x", "1.5", "1.5.0").behind).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("normalizes the v prefix out of the report", () => {
|
|
61
|
+
expect(compare("x", "v1.0.0", "v1.0.1")).toMatchObject({ pinned: "1.0.0", latest: "1.0.1" });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("unpinned", () => {
|
|
66
|
+
test("flags a floating tag", () => {
|
|
67
|
+
expect(unpinned([spec({ image: "floci/floci:latest" })]).map((s) => s.name)).toEqual(["chant-x"]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("flags an untagged image", () => {
|
|
71
|
+
expect(unpinned([spec({ image: "floci/floci" })])).toHaveLength(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("a pinned image is not flagged", () => {
|
|
75
|
+
expect(unpinned([spec()])).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("checkFreshness", () => {
|
|
80
|
+
const fakeFetch = (tag: string) =>
|
|
81
|
+
(async () => ({ ok: true, json: async () => ({ tag_name: tag }) })) as unknown as typeof fetch;
|
|
82
|
+
|
|
83
|
+
test("checks each spec that declares an upstream", async () => {
|
|
84
|
+
const results = await checkFreshness(
|
|
85
|
+
[spec({ name: "a", upstream: { repo: "o/a" } }), spec({ name: "b", upstream: { repo: "o/b" } })],
|
|
86
|
+
fakeFetch("1.0.0"),
|
|
87
|
+
);
|
|
88
|
+
expect(results.map((r) => r.name)).toEqual(["a", "b"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("skips a spec with no upstream — a locally built image has no release feed", async () => {
|
|
92
|
+
expect(await checkFreshness([spec()], fakeFetch("9.9.9"))).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("skips a spec whose image carries no version", async () => {
|
|
96
|
+
expect(
|
|
97
|
+
await checkFreshness([spec({ image: "floci/floci", upstream: { repo: "o/a" } })], fakeFetch("1.0.0")),
|
|
98
|
+
).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("reports behind when the release is newer than the pin", async () => {
|
|
102
|
+
const [result] = await checkFreshness(
|
|
103
|
+
[spec({ image: "org/x:1.0.0", upstream: { repo: "o/x" } })],
|
|
104
|
+
fakeFetch("1.2.0"),
|
|
105
|
+
);
|
|
106
|
+
expect(result).toMatchObject({ pinned: "1.0.0", latest: "1.2.0", behind: true });
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("latestRelease", () => {
|
|
111
|
+
test("returns the tag name", async () => {
|
|
112
|
+
const fetchImpl = (async () => ({ ok: true, json: async () => ({ tag_name: "v2.1.0" }) })) as unknown as typeof fetch;
|
|
113
|
+
expect(await latestRelease("o/r", fetchImpl)).toBe("v2.1.0");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("throws on a non-ok response rather than reporting a bogus version", async () => {
|
|
117
|
+
const fetchImpl = (async () => ({ ok: false, status: 404 })) as unknown as typeof fetch;
|
|
118
|
+
await expect(latestRelease("o/r", fetchImpl)).rejects.toThrow("HTTP 404");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("throws when the release has no tag", async () => {
|
|
122
|
+
const fetchImpl = (async () => ({ ok: true, json: async () => ({}) })) as unknown as typeof fetch;
|
|
123
|
+
await expect(latestRelease("o/r", fetchImpl)).rejects.toThrow("no tag_name");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("formatResult", () => {
|
|
128
|
+
test("marks a behind pin", () => {
|
|
129
|
+
expect(formatResult(compare("chant-floci", "1.0.0", "1.1.0"))).toContain("behind");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("marks a current pin", () => {
|
|
133
|
+
expect(formatResult(compare("chant-floci", "1.1.0", "1.1.0"))).toContain("current");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How far behind an emulator's pinned image is (#1345, generalizing #808 T2).
|
|
3
|
+
*
|
|
4
|
+
* This began as `lexicons/fly/src/emulator-freshness.ts`, checking fly's two
|
|
5
|
+
* pins against their GitHub releases. The other three emulators — Floci for aws,
|
|
6
|
+
* floci-az, floci-gcp — ran `:latest`, so there was nothing to be behind and
|
|
7
|
+
* nothing to check: an image could change underneath a passing local test suite
|
|
8
|
+
* with no record in the repo of what moved.
|
|
9
|
+
*
|
|
10
|
+
* With the pin and its upstream declared on {@link EmulatorSpec}, the check
|
|
11
|
+
* covers every emulator any lexicon ships, and a new one is included by
|
|
12
|
+
* declaring `upstream` rather than by editing a list here.
|
|
13
|
+
*
|
|
14
|
+
* Advisory, never gating. Per the bump policy (#808) a pin moves when a
|
|
15
|
+
* consuming test needs the newer emulator, not because a release happened.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { EmulatorSpec } from "./emulator-lifecycle";
|
|
19
|
+
|
|
20
|
+
export interface FreshnessResult {
|
|
21
|
+
/** The emulator's container name, e.g. `chant-mudflaps`. */
|
|
22
|
+
name: string;
|
|
23
|
+
/** Pinned version, no leading `v`. */
|
|
24
|
+
pinned: string;
|
|
25
|
+
/** Latest released version, no leading `v`. */
|
|
26
|
+
latest: string;
|
|
27
|
+
/** True when the latest release is newer than the pin. */
|
|
28
|
+
behind: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The version tag of an image ref (`ghcr.io/x/mudflaps:0.3.1` → `0.3.1`). */
|
|
32
|
+
export function parseVersion(image: string): string {
|
|
33
|
+
const lastColon = image.lastIndexOf(":");
|
|
34
|
+
const lastSlash = image.lastIndexOf("/");
|
|
35
|
+
// A registry port (`localhost:5000/x`) is not a tag.
|
|
36
|
+
if (lastColon < 0 || lastColon < lastSlash) return "";
|
|
37
|
+
return image.slice(lastColon + 1).replace(/^v/, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toParts(version: string): number[] {
|
|
41
|
+
return version.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Compare pinned against latest as dotted numeric versions. */
|
|
45
|
+
export function compare(name: string, pinned: string, latest: string): FreshnessResult {
|
|
46
|
+
const p = toParts(pinned);
|
|
47
|
+
const l = toParts(latest);
|
|
48
|
+
let behind = false;
|
|
49
|
+
for (let i = 0; i < Math.max(p.length, l.length); i++) {
|
|
50
|
+
const a = p[i] ?? 0;
|
|
51
|
+
const b = l[i] ?? 0;
|
|
52
|
+
if (b > a) {
|
|
53
|
+
behind = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (b < a) break;
|
|
57
|
+
}
|
|
58
|
+
return { name, pinned: pinned.replace(/^v/, ""), latest: latest.replace(/^v/, ""), behind };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The latest release tag for a repo, via the GitHub REST API. */
|
|
62
|
+
export async function latestRelease(repo: string, fetchImpl: typeof fetch = fetch): Promise<string> {
|
|
63
|
+
const headers: Record<string, string> = { Accept: "application/vnd.github+json" };
|
|
64
|
+
if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
65
|
+
const res = await fetchImpl(`https://api.github.com/repos/${repo}/releases/latest`, { headers });
|
|
66
|
+
if (!res.ok) throw new Error(`releases/latest ${repo}: HTTP ${res.status}`);
|
|
67
|
+
const body = (await res.json()) as { tag_name?: string };
|
|
68
|
+
if (!body.tag_name) throw new Error(`releases/latest ${repo}: no tag_name`);
|
|
69
|
+
return body.tag_name;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check every spec that declares an upstream. A spec without one is skipped
|
|
74
|
+
* rather than reported — an emulator built from a local image has no release
|
|
75
|
+
* feed to be behind.
|
|
76
|
+
*/
|
|
77
|
+
export async function checkFreshness(
|
|
78
|
+
specs: readonly EmulatorSpec[],
|
|
79
|
+
fetchImpl: typeof fetch = fetch,
|
|
80
|
+
): Promise<FreshnessResult[]> {
|
|
81
|
+
const pinned = specs.filter((s) => s.upstream?.repo && parseVersion(s.image));
|
|
82
|
+
return Promise.all(
|
|
83
|
+
pinned.map(async (s) =>
|
|
84
|
+
compare(s.name, parseVersion(s.image), await latestRelease(s.upstream!.repo, fetchImpl)),
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** One-line human summary of a result. */
|
|
90
|
+
export function formatResult(r: FreshnessResult): string {
|
|
91
|
+
return r.behind
|
|
92
|
+
? `⚠ ${r.name}: pinned ${r.pinned}, latest ${r.latest} — behind`
|
|
93
|
+
: `✓ ${r.name}: pinned ${r.pinned} is current (latest ${r.latest})`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Emulator images that carry no version tag — `:latest` or none at all. */
|
|
97
|
+
export function unpinned(specs: readonly EmulatorSpec[]): EmulatorSpec[] {
|
|
98
|
+
return specs.filter((s) => {
|
|
99
|
+
const version = parseVersion(s.image);
|
|
100
|
+
return version === "" || version === "latest";
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -22,6 +22,20 @@ export interface EmulatorSpec {
|
|
|
22
22
|
ready?: (healthBody: string) => boolean;
|
|
23
23
|
/** Extra `docker run` args inserted before the image (e.g. a socket mount). */
|
|
24
24
|
runArgs?: readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* Where {@link image} is published, so how far behind the pin is can be
|
|
27
|
+
* answered (#1345).
|
|
28
|
+
*
|
|
29
|
+
* fly pinned its two emulators and tracked their freshness; aws, azure and gcp
|
|
30
|
+
* ran `floci/*:latest`, which is the drift a pin exists to stop — a local test
|
|
31
|
+
* suite that passes today and fails tomorrow because an image moved underneath
|
|
32
|
+
* it, with nothing in the repo recording what changed. Declaring the upstream
|
|
33
|
+
* makes the check general instead of one lexicon's private tooling.
|
|
34
|
+
*/
|
|
35
|
+
upstream?: {
|
|
36
|
+
/** `owner/repo` whose latest GitHub release names the current version. */
|
|
37
|
+
repo: string;
|
|
38
|
+
};
|
|
25
39
|
}
|
|
26
40
|
|
|
27
41
|
/**
|
|
@@ -36,6 +50,41 @@ export interface EmulatorCapability {
|
|
|
36
50
|
env(endpoint: string): Record<string, string>;
|
|
37
51
|
}
|
|
38
52
|
|
|
53
|
+
/**
|
|
54
|
+
* What a plugin declares: one emulator, or several (#1345).
|
|
55
|
+
*
|
|
56
|
+
* fly ships two — mudflaps for the Machines API and spritzer for Sprites — and
|
|
57
|
+
* a single-spec field could describe only one of them, so both stayed
|
|
58
|
+
* unreachable from `chant emulator` while the repo's docs presented them as
|
|
59
|
+
* first-class local targets.
|
|
60
|
+
*/
|
|
61
|
+
export type EmulatorDeclaration = EmulatorCapability | readonly EmulatorCapability[];
|
|
62
|
+
|
|
63
|
+
/** Every emulator a plugin declares, normalized to a list. */
|
|
64
|
+
export function emulatorsOf(declaration: EmulatorDeclaration | undefined): readonly EmulatorCapability[] {
|
|
65
|
+
if (!declaration) return [];
|
|
66
|
+
return Array.isArray(declaration) ? declaration : [declaration as EmulatorCapability];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Sentinel that cannot collide with a real endpoint or a credential value. */
|
|
70
|
+
const ENDPOINT_PROBE = "chant-endpoint-probe://0";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The env vars whose value *is* the endpoint, as opposed to the credentials and
|
|
74
|
+
* region an emulator also needs (#1345).
|
|
75
|
+
*
|
|
76
|
+
* Derived by asking `env()` for a sentinel and keeping the keys that carry it,
|
|
77
|
+
* so a lexicon states the mapping once in the place it already states it. The
|
|
78
|
+
* alternative — `LEXICON_ENDPOINT_ENV_VAR`, a hand-maintained map in core — held
|
|
79
|
+
* two of the four lexicons that have one, and its module doc asserted azure had
|
|
80
|
+
* none while `describe-resources.ts` read `AZURE_ENDPOINT_URL` on every call.
|
|
81
|
+
*/
|
|
82
|
+
export function endpointEnvVars(capability: EmulatorCapability): string[] {
|
|
83
|
+
return Object.entries(capability.env(ENDPOINT_PROBE))
|
|
84
|
+
.filter(([, value]) => value === ENDPOINT_PROBE)
|
|
85
|
+
.map(([key]) => key);
|
|
86
|
+
}
|
|
87
|
+
|
|
39
88
|
/** Per-call overrides for {@link EmulatorLifecycle.up} / `runCommand`. */
|
|
40
89
|
export interface EmulatorUpArgs {
|
|
41
90
|
name?: string;
|
package/src/op/index.ts
CHANGED
|
@@ -9,8 +9,10 @@ export { Op, phase, activity, gate, build, kubectlApply, helmInstall, waitForSta
|
|
|
9
9
|
spritesUp, spritesDown } from "./builders";
|
|
10
10
|
export { OpResource } from "./resource";
|
|
11
11
|
export { safeHeartbeat, sleep } from "./activity-runtime";
|
|
12
|
-
export { emulatorLifecycle } from "./emulator-lifecycle";
|
|
13
|
-
export type { EmulatorSpec, EmulatorCapability, EmulatorUpArgs, EmulatorLifecycle } from "./emulator-lifecycle";
|
|
12
|
+
export { emulatorLifecycle, emulatorsOf, endpointEnvVars } from "./emulator-lifecycle";
|
|
13
|
+
export type { EmulatorSpec, EmulatorCapability, EmulatorDeclaration, EmulatorUpArgs, EmulatorLifecycle } from "./emulator-lifecycle";
|
|
14
|
+
export { checkFreshness, compare, formatResult, latestRelease, parseVersion, unpinned } from "./emulator-freshness";
|
|
15
|
+
export type { FreshnessResult } from "./emulator-freshness";
|
|
14
16
|
export type { OpConfig, PhaseDefinition, StepDefinition, ActivityStep, GateStep } from "./types";
|
|
15
17
|
export { discoverOps } from "./discover";
|
|
16
18
|
export type { DiscoveredOp, OpDiscoveryResult } from "./discover";
|
package/src/ownership.ts
CHANGED
|
@@ -128,3 +128,44 @@ export function readOwnership(
|
|
|
128
128
|
env: typeof env === "string" ? env : undefined,
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A read path that can resolve an ownership verdict from the marker (#1348).
|
|
134
|
+
*
|
|
135
|
+
* Per-path rather than per-lexicon because the answer genuinely differs by
|
|
136
|
+
* path: aws stamps tags at synthesis and reads them on the deep observation and
|
|
137
|
+
* on live export, but `describeResources` is sourced from
|
|
138
|
+
* `describe-stack-resources`, which returns no tags at all — so an `owned: true`
|
|
139
|
+
* thin read against aws can only answer `unknown`.
|
|
140
|
+
*/
|
|
141
|
+
export type OwnershipReadPath = "describeResources" | "observeResourcesDeep" | "exportResources";
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Where a lexicon can stamp and read chant's ownership marker (#1348).
|
|
145
|
+
*
|
|
146
|
+
* `ResourceMetadata.ownership` documents an obligation — a lexicon with no
|
|
147
|
+
* marker channel on a path must stamp `unknown` rather than degrade silently,
|
|
148
|
+
* because the change set never escalates `unknown` to a delete — and that
|
|
149
|
+
* obligation had no type, no declaration, and no check. A caller could not
|
|
150
|
+
* learn whether `owned: true` was answerable except by asking and reading a
|
|
151
|
+
* warning on stderr afterwards, which is invisible to `lifecycle plan`, which
|
|
152
|
+
* is where the wrong delete gets proposed.
|
|
153
|
+
*
|
|
154
|
+
* Absent means the lexicon has no marker channel at all: every verdict it
|
|
155
|
+
* returns must be `unknown`. Declaring one is a claim the conformance suite
|
|
156
|
+
* checks — on a declared path, verdicts must be `owned` or `foreign`.
|
|
157
|
+
*/
|
|
158
|
+
export interface OwnershipChannel {
|
|
159
|
+
/** The provider-native keys this lexicon stamps into. */
|
|
160
|
+
readonly keys: ChannelKeys;
|
|
161
|
+
/** The read paths that resolve a verdict from the marker. */
|
|
162
|
+
readonly reads: readonly OwnershipReadPath[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Whether this lexicon resolves a real verdict on `path`, or can only say `unknown`. */
|
|
166
|
+
export function resolvesOwnershipOn(
|
|
167
|
+
channel: OwnershipChannel | undefined,
|
|
168
|
+
path: OwnershipReadPath,
|
|
169
|
+
): boolean {
|
|
170
|
+
return channel?.reads.includes(path) ?? false;
|
|
171
|
+
}
|
package/src/serializer.ts
CHANGED
|
@@ -52,6 +52,22 @@ export interface Serializer {
|
|
|
52
52
|
*/
|
|
53
53
|
rulePrefix: string;
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Further id families this lexicon owns, beyond {@link rulePrefix} (#1349).
|
|
57
|
+
*
|
|
58
|
+
* The prefix exists so ids do not collide when several lexicons are loaded
|
|
59
|
+
* together — forgejo wraps github's rules as `WFJ-GHA0xx` for exactly that
|
|
60
|
+
* reason. It was declared and checked by nothing, and k8s quietly shipped
|
|
61
|
+
* five `ARGO0xx` checks outside its own `WK8`.
|
|
62
|
+
*
|
|
63
|
+
* A second family is sometimes right: Argo CD is a distinct product surface
|
|
64
|
+
* that happens to be covered by the k8s lexicon, and renaming published ids
|
|
65
|
+
* would break every `chant-disable ARGO001` in the wild. Declaring it keeps
|
|
66
|
+
* the collision guarantee while allowing the split — an undeclared family is
|
|
67
|
+
* a tier-1 failure.
|
|
68
|
+
*/
|
|
69
|
+
extraRulePrefixes?: readonly string[];
|
|
70
|
+
|
|
55
71
|
/**
|
|
56
72
|
* Serializes the entities to a string representation
|
|
57
73
|
* @param entities - Map of entity name to Declarable entity
|