@intentius/chant 0.66.1 → 0.67.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.
Files changed (36) hide show
  1. package/dist/behaviour-engine.d.ts +212 -0
  2. package/dist/behaviour-engine.d.ts.map +1 -0
  3. package/dist/behaviour-http.d.ts +1 -1
  4. package/dist/behaviour-http.d.ts.map +1 -1
  5. package/dist/behaviour-kinds.d.ts +220 -0
  6. package/dist/behaviour-kinds.d.ts.map +1 -0
  7. package/dist/behaviour-predict.d.ts +83 -0
  8. package/dist/behaviour-predict.d.ts.map +1 -0
  9. package/dist/behaviour-request.d.ts +141 -0
  10. package/dist/behaviour-request.d.ts.map +1 -0
  11. package/dist/behaviour.d.ts +7 -7
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/lexicon.d.ts +19 -0
  15. package/dist/lexicon.d.ts.map +1 -1
  16. package/dist/op/activities/index.d.ts +1 -1
  17. package/dist/op/activities/index.d.ts.map +1 -1
  18. package/dist/op/activities/predict-behaviour.d.ts +12 -5
  19. package/dist/op/activities/predict-behaviour.d.ts.map +1 -1
  20. package/package.json +1 -1
  21. package/src/behaviour-delta.test.ts +8 -8
  22. package/src/behaviour-engine.ts +478 -0
  23. package/src/behaviour-http.ts +1 -1
  24. package/src/behaviour-kinds.test.ts +235 -0
  25. package/src/behaviour-kinds.ts +324 -0
  26. package/src/behaviour-predict.ts +253 -0
  27. package/src/behaviour-request.ts +294 -0
  28. package/src/behaviour.ts +7 -7
  29. package/src/cli/handlers/scenario.test.ts +1 -1
  30. package/src/cli/handlers/scenario.ts +1 -1
  31. package/src/index.ts +1 -0
  32. package/src/lexicon.ts +20 -0
  33. package/src/lifecycle/scenario-cost.test.ts +2 -2
  34. package/src/op/activities/index.ts +1 -2
  35. package/src/op/activities/predict-behaviour.test.ts +20 -7
  36. package/src/op/activities/predict-behaviour.ts +32 -30
@@ -105,12 +105,12 @@ describe("evaluateScenario — cost against the fixture's recorded prediction",
105
105
  });
106
106
 
107
107
  test("a fixture whose prediction is a refusal fails with the refusal's reason, never a pass on nothing", () => {
108
- const verdict = evaluateScenario(EMPTY, { cost: { maxPerHour: 1e9, currency: "USD" } }, { result: noBehaviourEngineRefusal("augur") });
108
+ const verdict = evaluateScenario(EMPTY, { cost: { maxPerHour: 1e9, currency: "USD" } }, { result: noBehaviourEngineRefusal("chant") });
109
109
  expect(verdict.pass).toBe(false);
110
110
  expect(cost(verdict).detail).toContain("the fixture's prediction is a refusal (no-engine)");
111
111
  expect(cost(verdict).detail).toContain("Set CHANT_BEHAVIOUR_ENGINE to the engine's address.");
112
112
 
113
- const broke = outOfCreditBehaviourEngineRefusal("augur", { value: "engine", source: "CHANT_BEHAVIOUR_ENGINE" }, "balance 0");
113
+ const broke = outOfCreditBehaviourEngineRefusal("chant", { value: "engine", source: "CHANT_BEHAVIOUR_ENGINE" }, "balance 0");
114
114
  expect(cost(evaluateScenario(EMPTY, { cost: { maxPerHour: 1e9, currency: "USD" } }, { result: broke })).detail).toContain("engine-out-of-credit");
115
115
  });
116
116
 
@@ -137,8 +137,7 @@ export {
137
137
  baseRefFrom,
138
138
  headRefFrom,
139
139
  noBaseRefMessage,
140
- noPredictingLexiconMessage,
141
- ambiguousPredictorMessage,
140
+ noContributedRowsMessage,
142
141
  } from "./predict-behaviour";
143
142
  export type {
144
143
  PredictBehaviourArgs,
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { describe, expect, test, vi } from "vitest";
8
+ import { coverageFor, type BehaviourKinds } from "../../behaviour-kinds";
8
9
  import {
9
10
  behaviourReport,
10
11
  noBehaviourEngineRefusal,
@@ -15,14 +16,13 @@ import {
15
16
  } from "../../behaviour";
16
17
  import { commentMarker, issueMarker, type ReconcilePrArgs, type ReconcileResult } from "./reconcile";
17
18
  import {
18
- ambiguousPredictorMessage,
19
19
  baseRefFrom,
20
20
  behaviourFindingMarker,
21
21
  createBehaviourFinding,
22
22
  declaredEdgeCoverage,
23
23
  headRefFrom,
24
24
  noBaseRefMessage,
25
- noPredictingLexiconMessage,
25
+ noContributedRowsMessage,
26
26
  type BaseCheckout,
27
27
  } from "./predict-behaviour";
28
28
 
@@ -97,12 +97,25 @@ describe("declaredEdgeCoverage — never complete on the declared path", () => {
97
97
 
98
98
  describe("the refusals for a project that cannot predict", () => {
99
99
  test("no predicting lexicon names the configured ones and what to add", () => {
100
- expect(noPredictingLexiconMessage(["aws", "k8s"])).toMatch(/\(aws, k8s\).*augur.*CHANT_BEHAVIOUR_ENGINE/s);
101
- expect(noPredictingLexiconMessage([])).toContain("(none)");
100
+ // Not an error any more: with rows contributed per lexicon there is
101
+ // nothing to choose between, and an estate whose lexicons contribute no
102
+ // rows is a real state with a real answer rather than a broken command
103
+ // (#2382).
104
+ expect(noContributedRowsMessage(["aws", "k8s"])).toMatch(/\(aws, k8s\).*behaviourKinds.*CHANT_BEHAVIOUR_ENGINE/s);
105
+ expect(noContributedRowsMessage([])).toContain("(none)");
106
+ expect(noContributedRowsMessage(["aws"])).toContain("unknown-type");
102
107
  });
103
108
 
104
- test("two predicting lexicons is refused rather than merged", () => {
105
- expect(ambiguousPredictorMessage(["augur", "other"])).toMatch(/2 configured lexicons.*\(augur, other\)/);
109
+ test("two lexicons contributing rows is ordinary, not a conflict", () => {
110
+ // The old shape refused here, because two lexicons implementing
111
+ // predictBehaviour() meant two engines pricing one estate. Rows do not
112
+ // have that problem: each lexicon speaks only about its own entity types,
113
+ // so contributions are additive and there is nothing to merge.
114
+ const aws: BehaviourKinds = { provider: "aws", prefixes: ["AWS::"], mapped: { "AWS::S3::Bucket": { kind: "object-store" } } };
115
+ const k8s: BehaviourKinds = { provider: "kubernetes", prefixes: ["K8s::"], mapped: { "K8s::Core::Pod": { kind: "compute" } } };
116
+ expect(coverageFor([aws, k8s], "AWS::S3::Bucket").status).toBe("mapped");
117
+ expect(coverageFor([aws, k8s], "K8s::Core::Pod").status).toBe("mapped");
118
+ expect(coverageFor([k8s, aws], "AWS::S3::Bucket").status).toBe("mapped");
106
119
  });
107
120
  });
108
121
 
@@ -198,7 +211,7 @@ describe("behaviourFinding — predicts both sides, differences them, and posts
198
211
  });
199
212
 
200
213
  test("a refusal on the base side posts a finding that says no prediction, and flags the run", async () => {
201
- const { run, posted } = harness({ base: noBehaviourEngineRefusal("augur"), head: report({ db: figure(0.1) }) });
214
+ const { run, posted } = harness({ base: noBehaviourEngineRefusal("chant"), head: report({ db: figure(0.1) }) });
202
215
  const result = await run({ environment: "prod", traffic: TRAFFIC, op: "pr-behaviour" });
203
216
  expect(result.refused).toBe(true);
204
217
  expect(result.finding.kind).toBe("no-prediction");
@@ -55,7 +55,7 @@
55
55
  * `partial` either, because `partial` has to name a gap as `dangling` or
56
56
  * `unresolvedKinds`, and the gap here is neither — every reference resolved,
57
57
  * and the generic builder has no vocabulary for "this kind is a boundary
58
- * whose containment is missing" (augur's own fixture names its boundary
58
+ * whose containment is missing" (the request fixture names its boundary
59
59
  * kinds by hand, which is lexicon knowledge core does not have). An earlier
60
60
  * draft named every kind no reference touched, and named a queue nobody
61
61
  * references as "unresolved", which it is not. So the claim is `unknown`,
@@ -81,6 +81,8 @@ import {
81
81
  type BehaviourDelta,
82
82
  } from "../../behaviour-delta";
83
83
  import type { LexiconPlugin } from "../../lexicon";
84
+ import type { BehaviourKinds } from "../../behaviour-kinds";
85
+ import { createBehaviourPredict } from "../../behaviour-predict";
84
86
  import type { SerializerResult } from "../../serializer";
85
87
  import { markerSlug, reconcilePr, suppliedMarker, type ReconcileResult } from "./reconcile";
86
88
 
@@ -107,22 +109,23 @@ export interface PredictBehaviourArgs {
107
109
  owned?: boolean;
108
110
  }
109
111
 
110
- /** What the activity says when no configured lexicon implements the fourth method. */
111
- export function noPredictingLexiconMessage(lexicons: readonly string[]): string {
112
+ /**
113
+ * What the activity says when nothing has contributed coverage rows.
114
+ *
115
+ * Not an error and not a refusal: the prediction runs, every declared entity
116
+ * is withheld as `unknown-type`, and this is what the report's own detail says
117
+ * one level down. Kept as a message rather than a throw because an estate
118
+ * whose lexicons contribute no rows is a real state with a real answer — "no
119
+ * row says what any of these are" — and a throw would turn it into a broken
120
+ * command (#2382).
121
+ */
122
+ export function noContributedRowsMessage(lexicons: readonly string[]): string {
112
123
  const configured = lexicons.length > 0 ? lexicons.join(", ") : "(none)";
113
124
  return (
114
- "predictBehaviour has the estate to predict and no lexicon to predict it with: none of the configured " +
115
- `lexicons (${configured}) implements predictBehaviour(). Add augur to \`lexicons\` in chant.config.ts — it ` +
116
- "reads whatever the other lexicons declare — and set CHANT_BEHAVIOUR_ENGINE to the engine's address."
117
- );
118
- }
119
-
120
- /** What the activity says when more than one configured lexicon predicts. */
121
- export function ambiguousPredictorMessage(names: readonly string[]): string {
122
- return (
123
- `predictBehaviour found ${names.length} configured lexicons that implement predictBehaviour() (${names.join(", ")}) ` +
124
- "and predicts with one. Two engines pricing one estate is a merge this activity does not do, because an entity " +
125
- "priced twice has two provenances and one row; configure one predicting lexicon."
125
+ "predictBehaviour has the estate to predict and no coverage rows to read it with: none of the " +
126
+ `configured lexicons (${configured}) contributes \`behaviourKinds\`. Every entity will be reported ` +
127
+ "unpredicted as `unknown-type`. Set CHANT_BEHAVIOUR_ENGINE to the engine's address, and add rows " +
128
+ "in the lexicon that owns the entity types you expect priced."
126
129
  );
127
130
  }
128
131
 
@@ -157,10 +160,10 @@ export async function predictDeclared(projectPath: string, args: PredictBehaviou
157
160
  const { config } = await loadChantConfigUpward(projectPath);
158
161
  const lexicons = await resolveProjectLexicons(projectPath);
159
162
  const plugins = (await loadPlugins(lexicons)) as LexiconPlugin[];
160
- const predictors = plugins.filter((p) => typeof p.predictBehaviour === "function");
161
- if (predictors.length === 0) throw new Error(noPredictingLexiconMessage(lexicons));
162
- if (predictors.length > 1) throw new Error(ambiguousPredictorMessage(predictors.map((p) => p.name)));
163
- const [predictor] = predictors;
163
+ // Rows, not a predictor. Since #2382 core predicts and each lexicon says
164
+ // only what its own entity types mean, so there is nothing to choose between
165
+ // and nothing to refuse when two lexicons both answer — they are additive.
166
+ const kinds = plugins.map((p) => p.behaviourKinds).filter((k): k is BehaviourKinds => k !== undefined);
164
167
 
165
168
  const sourceDir = resolve(projectPath, config.sourceDir ?? ".");
166
169
  const result = await build(sourceDir, plugins.map((p) => p.serializer));
@@ -169,11 +172,10 @@ export async function predictDeclared(projectPath: string, args: PredictBehaviou
169
172
  throw new Error(`predictBehaviour: the project under ${projectPath} did not build: ${messages.join("; ")}`);
170
173
  }
171
174
 
172
- // Every declared entity with a type, the way augur's own request fixture
173
- // assembles it: the predicting lexicon decides what reaches the engine and
174
- // what is declared unmapped, and it can only decide about what it is
175
- // handed. Filtering here to "resources" would silently drop the kinds the
176
- // coverage table exists to name.
175
+ // Every declared entity with a type. The contributed rows decide what
176
+ // reaches the engine and what is declared unmapped, and they can only decide
177
+ // about what they are handed: filtering here to "resources" would silently
178
+ // drop the kinds the coverage rows exist to name.
177
179
  const entities = new Map<string, { entityType: string; props: Record<string, unknown> }>();
178
180
  for (const [name, entity] of result.entities) {
179
181
  const declarable = entity as { entityType?: unknown; props?: unknown };
@@ -185,11 +187,11 @@ export async function predictDeclared(projectPath: string, args: PredictBehaviou
185
187
  }
186
188
  const edges = buildGraphIr(result.entities, sourceDir).edges;
187
189
 
188
- // The predicting lexicon's own serialized output, as the deep read is handed
189
- // its lexicon's — a string, not a path (cli/handlers/lifecycle.ts). augur
190
- // reads nothing off it; the mirror is kept so a lexicon that does gets what
191
- // the other three methods get.
192
- const raw = result.outputs.get(predictor.name);
190
+ // The build output the other three reads are handed — a string, not a path
191
+ // (cli/handlers/lifecycle.ts). Nothing on this path reads it; the mirror is
192
+ // kept so the four methods take the same shape. With no predicting lexicon
193
+ // to take it from, it is the first output the build produced, or empty.
194
+ const raw = [...result.outputs.values()][0];
193
195
  const buildOutput = raw === undefined ? "" : typeof raw === "string" ? raw : (raw as SerializerResult).primary;
194
196
 
195
197
  const options: PredictBehaviourOptions = {
@@ -204,7 +206,7 @@ export async function predictDeclared(projectPath: string, args: PredictBehaviou
204
206
  edges,
205
207
  edgeCoverage: declaredEdgeCoverage(),
206
208
  };
207
- const answer = await predictor.predictBehaviour!(options);
209
+ const answer = await createBehaviourPredict({ kinds })(options);
208
210
  // On arrival, with the names that were asked — `behaviourReport` checks the
209
211
  // ordinary route and says a consumer that needs the guarantee checks again.
210
212
  return validateBehaviourResult(answer, options.entityNames);