@intentius/chant-lexicon-fountain 0.45.0 → 0.49.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/audit-entities.d.ts +21 -0
- package/dist/audit-entities.d.ts.map +1 -0
- package/dist/coverage.d.ts +7 -0
- package/dist/coverage.d.ts.map +1 -1
- package/dist/detect.d.ts +5 -0
- package/dist/detect.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.d.ts +11 -7
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/audit-entities.test.ts +157 -0
- package/src/audit-entities.ts +43 -0
- package/src/codegen/docs.ts +1 -1
- package/src/coverage.test.ts +33 -2
- package/src/coverage.ts +17 -0
- package/src/detect.ts +6 -0
- package/src/lint/audit-catalog.ts +19 -9
- package/src/plugin.test.ts +17 -4
- package/src/plugin.ts +13 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse-to-graph for `chant audit` (#1567).
|
|
3
|
+
*
|
|
4
|
+
* Audit discovery classifies standalone fountain manifests (#1566); this
|
|
5
|
+
* module turns one classified file's content back into the entity graph the
|
|
6
|
+
* FTN post-synth checks read (`ctx.entities`), so the same graph-reading
|
|
7
|
+
* rules that fire on `chant build` fire on an audit of hand-written
|
|
8
|
+
* `fountain apply` YAML — one implementation per rule, no output-reading
|
|
9
|
+
* variants to drift.
|
|
10
|
+
*
|
|
11
|
+
* Tolerant by contract: the audit runs against any repo, so a malformed
|
|
12
|
+
* document contributes no entities instead of throwing, and documents are
|
|
13
|
+
* parsed one at a time so one bad document doesn't take down the file's
|
|
14
|
+
* others. A name declared twice keeps both declarations in the map (the
|
|
15
|
+
* second gets a `#n`-suffixed key), so nothing is silently dropped and
|
|
16
|
+
* FTN017's duplicate-name detection still sees the collision.
|
|
17
|
+
*/
|
|
18
|
+
import type { Declarable } from "@intentius/chant";
|
|
19
|
+
/** Parse standalone fountain YAML (or a fountain-plan.json) into `ctx.entities`. */
|
|
20
|
+
export declare function fountainAuditEntities(content: string): Map<string, Declarable>;
|
|
21
|
+
//# sourceMappingURL=audit-entities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-entities.d.ts","sourceRoot":"","sources":["../src/audit-entities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAoB9E"}
|
package/dist/coverage.d.ts
CHANGED
|
@@ -65,6 +65,13 @@ export declare function computeFountainCoverage(specJSON: string | Buffer, surfa
|
|
|
65
65
|
export declare function formatSummary(report: FountainCoverageReport): string;
|
|
66
66
|
export declare function formatVerbose(report: FountainCoverageReport): string;
|
|
67
67
|
/** Run coverage analysis for the fountain lexicon. */
|
|
68
|
+
/**
|
|
69
|
+
* The offline path behind `coverageReport()` (#1330): the same computation
|
|
70
|
+
* `coverage.test.ts` runs, over the committed spec snapshot and surface
|
|
71
|
+
* baseline. Never `fetchSchemas()` — check-lexicon runs on every PR and must
|
|
72
|
+
* not do network I/O.
|
|
73
|
+
*/
|
|
74
|
+
export declare function coverageReportFromSnapshots(basePath?: string): FountainCoverageReport;
|
|
68
75
|
export declare function analyzeFountainCoverage(opts?: {
|
|
69
76
|
basePath?: string;
|
|
70
77
|
verbose?: boolean;
|
package/dist/coverage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,uDAAuD;AACvD,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAQjD,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAUxD,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,yEAAyE;IACzE,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,8CAA8C;IAC9C,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,iEAAiE;IACjE,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,+EAA+E;IAC/E,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CAC7D;AAOD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,OAAO,EAAE,eAAe,GACvB,sBAAsB,CAkDxB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAGpE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAkCpE;AAED,sDAAsD;AACtD,wBAAsB,uBAAuB,CAAC,IAAI,CAAC,EAAE;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAsBlC"}
|
|
1
|
+
{"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,uDAAuD;AACvD,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAQjD,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAUxD,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,yEAAyE;IACzE,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,8CAA8C;IAC9C,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,iEAAiE;IACjE,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,+EAA+E;IAC/E,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CAC7D;AAOD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,OAAO,EAAE,eAAe,GACvB,sBAAsB,CAkDxB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAGpE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAkCpE;AAED,sDAAsD;AACtD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,sBAAsB,CASrF;AAED,wBAAsB,uBAAuB,CAAC,IAAI,CAAC,EAAE;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAsBlC"}
|
package/dist/detect.d.ts
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
export declare function isFountainManifest(doc: unknown): boolean;
|
|
6
6
|
/** The serializer's fountain-plan.json shape: entity name → { kind, spec }. */
|
|
7
7
|
export declare function isFountainPlan(data: unknown): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Canonical `detectTemplate` name under `@…/detect` — what edge callers
|
|
10
|
+
* (and core's detect-bundle guard) import for content detection.
|
|
11
|
+
*/
|
|
12
|
+
export { detectFountainTemplate as detectTemplate };
|
|
8
13
|
/** Template detection for the plugin: raw string input (YAML or JSON). */
|
|
9
14
|
export declare function detectFountainTemplate(data: unknown): boolean;
|
|
10
15
|
//# sourceMappingURL=detect.d.ts.map
|
package/dist/detect.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,sFAAsF;AACtF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAIxD;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CASrD;AAED,0EAA0E;AAC1E,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAU7D"}
|
|
1
|
+
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,sFAAsF;AACtF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAIxD;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CASrD;AAED;;;GAGG;AACH,OAAO,EAAE,sBAAsB,IAAI,cAAc,EAAE,CAAC;AAEpD,0EAA0E;AAC1E,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAU7D"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "c4e4aef153956f7e33b8732485af8e2bf16c7e57be9e99574982d2b56c7c461c",
|
|
5
5
|
"meta.json": "6666b7a77db9210a329219c6a5e107e5ae8794a34542f7310ce16d9bf95c64f5",
|
|
6
6
|
"types/index.d.ts": "1dfdcba184fffcf464dac7d1d71ef7abed2ae02a8d638c0de50e0b0317b7eb01",
|
|
7
7
|
"rules/ftn001-no-secret-literals.ts": "897a4ce1ec790b3c1540d32892603bd33ff4bf30eb6ba4cd2565dee356d4962e",
|
|
@@ -17,5 +17,5 @@
|
|
|
17
17
|
"skills/chant-fountain-secrets.md": "27e349a91589510a92e518c7d7824a4a322cab5ef242cf5799373a55cbfcd1cd",
|
|
18
18
|
"skills/chant-fountain-locked-sandboxes.md": "de82f06cb3a08ba6bf3ae45fb9869e21d6da18b9ebe0fc769da8aebaceea7dd1"
|
|
19
19
|
},
|
|
20
|
-
"composite": "
|
|
20
|
+
"composite": "6bd8f5f7bab081c4e3f249f92ac6bfa9a6dd3a94718bba6142046e5e00d8d9f3"
|
|
21
21
|
}
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
* The fountain lexicon's chant audit catalog — metadata for the FTN rules,
|
|
3
3
|
* contributed via `fountainPlugin.auditCatalog()` (#687).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* All of fountain's checks read the chant model (`ctx.entities`), not the
|
|
6
|
+
* emitted manifests (`ctx.outputs`), because the facts they need — which
|
|
7
|
+
* Environment an Agent references, which keys a Vault shadows — live in the
|
|
8
|
+
* typed graph and are flattened by the time YAML exists. They still fire on
|
|
9
|
+
* an audit of standalone fountain YAML: the audit parses classified
|
|
10
|
+
* `fountain.dev/v1` documents back into the entity graph via the plugin's
|
|
11
|
+
* `auditEntities` (parse-to-graph, #1567), so the post-synth entries are
|
|
12
|
+
* `yamlBased: true` — one rule implementation serves both paths. The one
|
|
13
|
+
* exception is FTN001, a declarative lint rule over TypeScript source that
|
|
14
|
+
* the audit never runs; it stays `yamlBased: false`. `auditRule()` hardcodes
|
|
15
|
+
* `yamlBased: true`, so these are constructed directly.
|
|
12
16
|
*/
|
|
13
17
|
import type { RuleMeta } from "@intentius/chant/audit/catalog";
|
|
14
18
|
export declare const fountainAuditCatalog: Record<string, RuleMeta>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit-catalog.d.ts","sourceRoot":"","sources":["../../src/lint/audit-catalog.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"audit-catalog.d.ts","sourceRoot":"","sources":["../../src/lint/audit-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAa,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AA6B1E,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAsEzD,CAAC"}
|
package/dist/manifest.json
CHANGED
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;AAuB7F;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,aA0J5B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-fountain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Fountain lexicon for chant — sandboxed agent environments, vaults, and agents as typed estate",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"bundle": "tsx src/package-cli.ts"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@intentius/chant": "^0.
|
|
53
|
+
"@intentius/chant": "^0.49.0",
|
|
54
54
|
"typescript": "^5.9.3"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { Declarable } from "@intentius/chant";
|
|
3
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
4
|
+
import { fountainAuditEntities } from "./audit-entities";
|
|
5
|
+
import { fountainSerializer } from "./serializer";
|
|
6
|
+
import { postSynthChecks } from "./lint/post-synth";
|
|
7
|
+
import { propsOf } from "./entity-props";
|
|
8
|
+
|
|
9
|
+
function ctxOf(entities: Map<string, Declarable>): PostSynthContext {
|
|
10
|
+
return {
|
|
11
|
+
outputs: new Map(),
|
|
12
|
+
entities,
|
|
13
|
+
buildResult: { outputs: new Map(), entities, warnings: [], errors: [], sourceFileCount: 0 },
|
|
14
|
+
} as unknown as PostSynthContext;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const runAll = (entities: Map<string, Declarable>) => postSynthChecks.flatMap((c) => c.check(ctxOf(entities)));
|
|
18
|
+
|
|
19
|
+
const DIRTY = `apiVersion: fountain.dev/v1
|
|
20
|
+
kind: Environment
|
|
21
|
+
metadata:
|
|
22
|
+
name: dev
|
|
23
|
+
spec:
|
|
24
|
+
networking_type: unrestricted
|
|
25
|
+
env_vars:
|
|
26
|
+
API_URL: https://api.example.com
|
|
27
|
+
AWS_SECRET_ACCESS_KEY: not-a-real-secret
|
|
28
|
+
---
|
|
29
|
+
apiVersion: fountain.dev/v1
|
|
30
|
+
kind: Vault
|
|
31
|
+
metadata:
|
|
32
|
+
name: staging
|
|
33
|
+
spec:
|
|
34
|
+
secrets:
|
|
35
|
+
- key: API_URL
|
|
36
|
+
value: https://staging.example.com
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const CLEAN = `apiVersion: fountain.dev/v1
|
|
40
|
+
kind: Environment
|
|
41
|
+
metadata:
|
|
42
|
+
name: dev
|
|
43
|
+
spec:
|
|
44
|
+
networking_type: limited
|
|
45
|
+
networking_config:
|
|
46
|
+
allowed_hosts:
|
|
47
|
+
- github.com
|
|
48
|
+
env_vars:
|
|
49
|
+
API_URL: https://api.example.com
|
|
50
|
+
---
|
|
51
|
+
apiVersion: fountain.dev/v1
|
|
52
|
+
kind: Agent
|
|
53
|
+
metadata:
|
|
54
|
+
name: researcher
|
|
55
|
+
spec:
|
|
56
|
+
model: anthropic/claude-sonnet-4-6
|
|
57
|
+
runtime: claude
|
|
58
|
+
environment: dev
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
describe("fountainAuditEntities (parse-to-graph, #1567)", () => {
|
|
62
|
+
it("parses manifests into entities keyed by metadata.name, props under .props", () => {
|
|
63
|
+
const entities = fountainAuditEntities(DIRTY);
|
|
64
|
+
expect([...entities.keys()].sort()).toEqual(["dev", "staging"]);
|
|
65
|
+
const dev = entities.get("dev")!;
|
|
66
|
+
expect(dev.entityType).toBe("Fountain::V1::Environment");
|
|
67
|
+
expect(propsOf(dev).networking_type).toBe("unrestricted");
|
|
68
|
+
expect(propsOf(dev).name).toBe("dev");
|
|
69
|
+
expect(entities.get("staging")!.entityType).toBe("Fountain::V1::Vault");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("the existing FTN checks fire unchanged over the parsed graph", () => {
|
|
73
|
+
const diags = runAll(fountainAuditEntities(DIRTY));
|
|
74
|
+
const ids = diags.map((d) => d.checkId).sort();
|
|
75
|
+
expect(ids).toContain("FTN011"); // unrestricted networking
|
|
76
|
+
expect(ids).toContain("FTN012"); // credential-shaped env_vars key
|
|
77
|
+
expect(ids).toContain("FTN014"); // vault shadows API_URL
|
|
78
|
+
expect(diags.find((d) => d.checkId === "FTN012")!.severity).toBe("error");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("a clean manifest set yields no findings", () => {
|
|
82
|
+
expect(runAll(fountainAuditEntities(CLEAN))).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("a name declared twice keeps both entities (suffixed key), so FTN017 sees the collision", () => {
|
|
86
|
+
const twice = `apiVersion: fountain.dev/v1
|
|
87
|
+
kind: Environment
|
|
88
|
+
metadata:
|
|
89
|
+
name: dev
|
|
90
|
+
spec:
|
|
91
|
+
networking_type: limited
|
|
92
|
+
---
|
|
93
|
+
apiVersion: fountain.dev/v1
|
|
94
|
+
kind: Environment
|
|
95
|
+
metadata:
|
|
96
|
+
name: dev
|
|
97
|
+
spec:
|
|
98
|
+
networking_type: limited
|
|
99
|
+
`;
|
|
100
|
+
const entities = fountainAuditEntities(twice);
|
|
101
|
+
expect([...entities.keys()].sort()).toEqual(["dev", "dev#2"]);
|
|
102
|
+
expect(runAll(entities).map((d) => d.checkId)).toContain("FTN017");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("a malformed document contributes nothing; the rest of the file still parses", () => {
|
|
106
|
+
const mixed = `{not yaml [
|
|
107
|
+
---
|
|
108
|
+
apiVersion: fountain.dev/v1
|
|
109
|
+
kind: Environment
|
|
110
|
+
metadata:
|
|
111
|
+
name: dev
|
|
112
|
+
spec:
|
|
113
|
+
networking_type: limited
|
|
114
|
+
`;
|
|
115
|
+
const entities = fountainAuditEntities(mixed);
|
|
116
|
+
expect([...entities.keys()]).toEqual(["dev"]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("non-fountain documents in the file are skipped, not misread", () => {
|
|
120
|
+
const entities = fountainAuditEntities("apiVersion: v1\nkind: Pod\nmetadata:\n name: p\n");
|
|
121
|
+
expect(entities.size).toBe(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("round-trips: serialize -> parse -> same entities (guards the #1286 class)", () => {
|
|
125
|
+
const entity = (entityType: string, props: Record<string, unknown>): Declarable =>
|
|
126
|
+
({ entityType, lexicon: "fountain", props }) as unknown as Declarable;
|
|
127
|
+
const src = new Map<string, Declarable>([
|
|
128
|
+
[
|
|
129
|
+
"dev",
|
|
130
|
+
entity("Fountain::V1::Environment", {
|
|
131
|
+
name: "dev",
|
|
132
|
+
networking_type: "limited",
|
|
133
|
+
networking_config: { allowed_hosts: ["github.com"] },
|
|
134
|
+
env_vars: { API_URL: "https://api.example.com" },
|
|
135
|
+
}),
|
|
136
|
+
],
|
|
137
|
+
[
|
|
138
|
+
"researcher",
|
|
139
|
+
entity("Fountain::V1::Agent", {
|
|
140
|
+
name: "researcher",
|
|
141
|
+
runtime: "claude",
|
|
142
|
+
model: "anthropic/claude-sonnet-4-6",
|
|
143
|
+
environment: "dev",
|
|
144
|
+
}),
|
|
145
|
+
],
|
|
146
|
+
]);
|
|
147
|
+
const parsed = fountainAuditEntities(fountainSerializer.serialize(src) as string);
|
|
148
|
+
expect([...parsed.keys()].sort()).toEqual(["dev", "researcher"]);
|
|
149
|
+
for (const [name, before] of src) {
|
|
150
|
+
const after = parsed.get(name)!;
|
|
151
|
+
expect(after.entityType).toBe(before.entityType);
|
|
152
|
+
expect(propsOf(after)).toEqual(propsOf(before));
|
|
153
|
+
}
|
|
154
|
+
// and the parsed graph is as quiet as the declared one
|
|
155
|
+
expect(runAll(parsed)).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse-to-graph for `chant audit` (#1567).
|
|
3
|
+
*
|
|
4
|
+
* Audit discovery classifies standalone fountain manifests (#1566); this
|
|
5
|
+
* module turns one classified file's content back into the entity graph the
|
|
6
|
+
* FTN post-synth checks read (`ctx.entities`), so the same graph-reading
|
|
7
|
+
* rules that fire on `chant build` fire on an audit of hand-written
|
|
8
|
+
* `fountain apply` YAML — one implementation per rule, no output-reading
|
|
9
|
+
* variants to drift.
|
|
10
|
+
*
|
|
11
|
+
* Tolerant by contract: the audit runs against any repo, so a malformed
|
|
12
|
+
* document contributes no entities instead of throwing, and documents are
|
|
13
|
+
* parsed one at a time so one bad document doesn't take down the file's
|
|
14
|
+
* others. A name declared twice keeps both declarations in the map (the
|
|
15
|
+
* second gets a `#n`-suffixed key), so nothing is silently dropped and
|
|
16
|
+
* FTN017's duplicate-name detection still sees the collision.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { Declarable } from "@intentius/chant";
|
|
20
|
+
import { FountainParser } from "./import/parser";
|
|
21
|
+
|
|
22
|
+
/** Parse standalone fountain YAML (or a fountain-plan.json) into `ctx.entities`. */
|
|
23
|
+
export function fountainAuditEntities(content: string): Map<string, Declarable> {
|
|
24
|
+
const entities = new Map<string, Declarable>();
|
|
25
|
+
const parser = new FountainParser();
|
|
26
|
+
for (const docText of content.split(/^---\s*$/m)) {
|
|
27
|
+
if (!docText.trim()) continue;
|
|
28
|
+
let resources;
|
|
29
|
+
try {
|
|
30
|
+
resources = parser.parse(docText).resources;
|
|
31
|
+
} catch {
|
|
32
|
+
continue; // malformed document — no entities, never a crash
|
|
33
|
+
}
|
|
34
|
+
for (const r of resources) {
|
|
35
|
+
let key = r.logicalId;
|
|
36
|
+
for (let n = 2; entities.has(key); n++) key = `${r.logicalId}#${n}`;
|
|
37
|
+
// The plain-object entity shape the checks' `propsOf` reads: authored
|
|
38
|
+
// props under `.props`, mirroring real createResource instances.
|
|
39
|
+
entities.set(key, { lexicon: "fountain", entityType: r.type, props: r.properties } as unknown as Declarable);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return entities;
|
|
43
|
+
}
|
package/src/codegen/docs.ts
CHANGED
|
@@ -45,7 +45,7 @@ export const helper = new Agent({
|
|
|
45
45
|
|
|
46
46
|
## The loop
|
|
47
47
|
|
|
48
|
-
1. \`chant build\` — synthesize and lint. The FTN rules catch open networking, credential literals, and unresolvable \`\${VAR}\` references before review.
|
|
48
|
+
1. \`chant build\` — synthesize and lint. The FTN rules catch open networking, credential literals, and unresolvable \`\${VAR}\` references before review. The same rules run via [\`chant audit\`](/chant/cli/audit/) over a repo of hand-written \`fountain apply\` manifests — no chant project needed; the documents are parsed back into the entity graph.
|
|
49
49
|
2. \`chant run <apply op>\` or call \`fountainApply\` — reconcile against the API. Idempotent by name.
|
|
50
50
|
3. \`chant lifecycle diff --live\` — drift. A UI edit to an owned Environment shows up here.
|
|
51
51
|
4. \`chant import --from\` — adopt UI-built resources into typed files.
|
package/src/coverage.test.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
3
|
import { join, dirname } from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
computeFountainCoverage,
|
|
7
|
+
coverageReportFromSnapshots,
|
|
8
|
+
formatVerbose,
|
|
9
|
+
EXCLUDED_KINDS,
|
|
10
|
+
UNSPECIFIED_ENDPOINTS,
|
|
11
|
+
} from "./coverage";
|
|
12
|
+
import { fountainPlugin } from "./plugin";
|
|
6
13
|
import { fetchSchemas } from "./spec/fetch";
|
|
7
14
|
|
|
8
15
|
const srcDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -63,6 +70,30 @@ describe("fountain coverage", () => {
|
|
|
63
70
|
});
|
|
64
71
|
});
|
|
65
72
|
|
|
73
|
+
// chant #1330 — the same accounting, exposed to check-lexicon through the
|
|
74
|
+
// plugin contract. It must agree with the direct computation above and must
|
|
75
|
+
// stay offline: check-lexicon runs on every PR.
|
|
76
|
+
describe("coverageReport plugin contract", () => {
|
|
77
|
+
it("reports no unaccounted kinds over the committed snapshots", async () => {
|
|
78
|
+
const report = await fountainPlugin.coverageReport!();
|
|
79
|
+
expect(report.unaccountedKinds).toEqual([]);
|
|
80
|
+
expect(report.unaccountedKinds).toEqual(
|
|
81
|
+
computeFountainCoverage(spec, surface).unaccountedKinds,
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("does no network I/O", async () => {
|
|
86
|
+
const spy = vi.spyOn(globalThis, "fetch");
|
|
87
|
+
try {
|
|
88
|
+
await fountainPlugin.coverageReport!();
|
|
89
|
+
coverageReportFromSnapshots();
|
|
90
|
+
expect(spy).not.toHaveBeenCalled();
|
|
91
|
+
} finally {
|
|
92
|
+
spy.mockRestore();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
66
97
|
describe("unspecified upstream endpoints", () => {
|
|
67
98
|
it("records a decision for every endpoint the spec cannot describe", () => {
|
|
68
99
|
// The point of the list is the reasoning, not the route string. An entry
|
package/src/coverage.ts
CHANGED
|
@@ -190,6 +190,23 @@ export function formatVerbose(report: FountainCoverageReport): string {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
/** Run coverage analysis for the fountain lexicon. */
|
|
193
|
+
/**
|
|
194
|
+
* The offline path behind `coverageReport()` (#1330): the same computation
|
|
195
|
+
* `coverage.test.ts` runs, over the committed spec snapshot and surface
|
|
196
|
+
* baseline. Never `fetchSchemas()` — check-lexicon runs on every PR and must
|
|
197
|
+
* not do network I/O.
|
|
198
|
+
*/
|
|
199
|
+
export function coverageReportFromSnapshots(basePath?: string): FountainCoverageReport {
|
|
200
|
+
const base = basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
|
|
201
|
+
|
|
202
|
+
const spec = readFileSync(join(base, "src", "spec", "fountain-openapi.snapshot.json"), "utf-8");
|
|
203
|
+
const surface = JSON.parse(
|
|
204
|
+
readFileSync(join(base, "surface.snapshot.json"), "utf-8"),
|
|
205
|
+
) as SurfaceSnapshot;
|
|
206
|
+
|
|
207
|
+
return computeFountainCoverage(spec, surface);
|
|
208
|
+
}
|
|
209
|
+
|
|
193
210
|
export async function analyzeFountainCoverage(opts?: {
|
|
194
211
|
basePath?: string;
|
|
195
212
|
verbose?: boolean;
|
package/src/detect.ts
CHANGED
|
@@ -23,6 +23,12 @@ export function isFountainPlan(data: unknown): boolean {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Canonical `detectTemplate` name under `@…/detect` — what edge callers
|
|
28
|
+
* (and core's detect-bundle guard) import for content detection.
|
|
29
|
+
*/
|
|
30
|
+
export { detectFountainTemplate as detectTemplate };
|
|
31
|
+
|
|
26
32
|
/** Template detection for the plugin: raw string input (YAML or JSON). */
|
|
27
33
|
export function detectFountainTemplate(data: unknown): boolean {
|
|
28
34
|
if (typeof data === "string") {
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
* The fountain lexicon's chant audit catalog — metadata for the FTN rules,
|
|
3
3
|
* contributed via `fountainPlugin.auditCatalog()` (#687).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* All of fountain's checks read the chant model (`ctx.entities`), not the
|
|
6
|
+
* emitted manifests (`ctx.outputs`), because the facts they need — which
|
|
7
|
+
* Environment an Agent references, which keys a Vault shadows — live in the
|
|
8
|
+
* typed graph and are flattened by the time YAML exists. They still fire on
|
|
9
|
+
* an audit of standalone fountain YAML: the audit parses classified
|
|
10
|
+
* `fountain.dev/v1` documents back into the entity graph via the plugin's
|
|
11
|
+
* `auditEntities` (parse-to-graph, #1567), so the post-synth entries are
|
|
12
|
+
* `yamlBased: true` — one rule implementation serves both paths. The one
|
|
13
|
+
* exception is FTN001, a declarative lint rule over TypeScript source that
|
|
14
|
+
* the audit never runs; it stays `yamlBased: false`. `auditRule()` hardcodes
|
|
15
|
+
* `yamlBased: true`, so these are constructed directly.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
import type { Authority, RuleMeta } from "@intentius/chant/audit/catalog";
|
|
@@ -23,7 +27,11 @@ const OWASP_LLM_INJECTION: Authority = {
|
|
|
23
27
|
url: "https://owasp.org/www-project-top-10-for-large-language-model-applications/",
|
|
24
28
|
};
|
|
25
29
|
|
|
26
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Entity-based rule. `yamlBased` defaults to true: the post-synth checks fire
|
|
32
|
+
* on audited standalone YAML via parse-to-graph (#1567). Pass false only for
|
|
33
|
+
* a rule the audit genuinely cannot run (FTN001, a source-level lint rule).
|
|
34
|
+
*/
|
|
27
35
|
function rule(
|
|
28
36
|
id: string,
|
|
29
37
|
tier: RuleMeta["tier"],
|
|
@@ -31,8 +39,9 @@ function rule(
|
|
|
31
39
|
title: string,
|
|
32
40
|
remediation: string,
|
|
33
41
|
authority?: Authority[],
|
|
42
|
+
yamlBased = true,
|
|
34
43
|
): RuleMeta {
|
|
35
|
-
return { id, tier, fixKind: "guidance", category, title, remediation, authority, yamlBased
|
|
44
|
+
return { id, tier, fixKind: "guidance", category, title, remediation, authority, yamlBased };
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
export const fountainAuditCatalog: Record<string, RuleMeta> = {
|
|
@@ -43,6 +52,7 @@ export const fountainAuditCatalog: Record<string, RuleMeta> = {
|
|
|
43
52
|
"Literal credential in a fountain declaration",
|
|
44
53
|
"Use a ${VAR} substitution reference or an environment secret; never a literal in source.",
|
|
45
54
|
[OWASP_LLM_INJECTION],
|
|
55
|
+
false, // lint rule over TypeScript source — the audit never runs it
|
|
46
56
|
),
|
|
47
57
|
FTN010: rule(
|
|
48
58
|
"FTN010",
|
package/src/plugin.test.ts
CHANGED
|
@@ -47,11 +47,24 @@ describe("fountain plugin", () => {
|
|
|
47
47
|
expect(ruleIds.sort()).toEqual(Object.keys(catalog).sort());
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
it("marks
|
|
50
|
+
it("marks exactly the checks the audit can run as yamlBased (#1567)", () => {
|
|
51
51
|
const catalog = fountainPlugin.auditCatalog?.() ?? {};
|
|
52
|
-
// Every
|
|
53
|
-
//
|
|
54
|
-
|
|
52
|
+
// Every post-synth check fires on standalone fountain YAML via
|
|
53
|
+
// parse-to-graph (auditEntities), so their entries are yamlBased. FTN001
|
|
54
|
+
// is a lint rule over TypeScript source the audit never runs — claiming
|
|
55
|
+
// it fires on YAML would misreport `chant audit --rules`.
|
|
56
|
+
const postSynthIds = new Set((fountainPlugin.postSynthChecks?.() ?? []).map((c) => c.id));
|
|
57
|
+
for (const [id, meta] of Object.entries(catalog)) {
|
|
58
|
+
expect(meta.yamlBased, id).toBe(postSynthIds.has(id));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("parses standalone manifests into the entity graph via auditEntities (#1567)", () => {
|
|
63
|
+
const entities = fountainPlugin.auditEntities?.(
|
|
64
|
+
"apiVersion: fountain.dev/v1\nkind: Environment\nmetadata:\n name: dev\nspec:\n networking_type: limited\n",
|
|
65
|
+
);
|
|
66
|
+
expect(entities?.size).toBe(1);
|
|
67
|
+
expect(entities?.get("dev")?.entityType).toBe("Fountain::V1::Environment");
|
|
55
68
|
});
|
|
56
69
|
|
|
57
70
|
it("scaffolds a closed sandbox by default", () => {
|
package/src/plugin.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { fountainSerializer } from "./serializer";
|
|
|
12
12
|
import { rules } from "./lint/rules";
|
|
13
13
|
import { postSynthChecks } from "./lint/post-synth";
|
|
14
14
|
import { fountainAuditCatalog } from "./lint/audit-catalog";
|
|
15
|
+
import { fountainAuditEntities } from "./audit-entities";
|
|
15
16
|
import { fountainReferenceCatalog } from "./reference-catalog";
|
|
16
17
|
import { detectFountainTemplate } from "./detect";
|
|
17
18
|
import { fountainInitTemplates } from "./init-templates";
|
|
@@ -63,6 +64,14 @@ export const fountainPlugin: LexiconPlugin = {
|
|
|
63
64
|
|
|
64
65
|
// ── Optional extensions ────────────────────────────────────
|
|
65
66
|
|
|
67
|
+
// #1330 — the fact check-lexicon gates on, computed offline from the
|
|
68
|
+
// committed spec snapshot + surface baseline. Never fetchSchemas():
|
|
69
|
+
// check-lexicon runs per-PR, no network.
|
|
70
|
+
async coverageReport(): Promise<{ unaccountedKinds?: string[] }> {
|
|
71
|
+
const { coverageReportFromSnapshots } = await import("./coverage");
|
|
72
|
+
return { unaccountedKinds: coverageReportFromSnapshots().unaccountedKinds };
|
|
73
|
+
},
|
|
74
|
+
|
|
66
75
|
lintRules() {
|
|
67
76
|
return rules;
|
|
68
77
|
},
|
|
@@ -73,6 +82,10 @@ export const fountainPlugin: LexiconPlugin = {
|
|
|
73
82
|
|
|
74
83
|
auditCatalog: () => fountainAuditCatalog,
|
|
75
84
|
|
|
85
|
+
auditEntities(content: string) {
|
|
86
|
+
return fountainAuditEntities(content);
|
|
87
|
+
},
|
|
88
|
+
|
|
76
89
|
skills: createSkillsLoader(import.meta.url, [
|
|
77
90
|
{
|
|
78
91
|
file: "chant-fountain.md",
|