@intentius/chant 0.93.0 → 0.95.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/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-plugins.d.ts +6 -6
- package/dist/cli/registry.d.ts +2 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/op/builders.d.ts +14 -3
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +6 -3
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/operator.d.ts +90 -0
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/steward-beside.d.ts +84 -0
- package/dist/op/steward-beside.d.ts.map +1 -0
- package/dist/op/steward.d.ts +87 -2
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/workspace/box-intent.d.ts +85 -0
- package/dist/workspace/box-intent.d.ts.map +1 -0
- package/dist/workspace/box-services.d.ts +31 -0
- package/dist/workspace/box-services.d.ts.map +1 -0
- package/dist/workspace/chant-migrations.d.ts +5 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -1
- package/dist/workspace/checks/boxes.d.ts +14 -1
- package/dist/workspace/checks/boxes.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +4 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +11 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/composites.d.ts +5 -1
- package/dist/workspace/composites.d.ts.map +1 -1
- package/dist/workspace/decision-points.schema.json +3 -3
- package/dist/workspace/declaration.d.ts +43 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +62 -1
- package/dist/workspace/graph-cache.d.ts +168 -0
- package/dist/workspace/graph-cache.d.ts.map +1 -0
- package/dist/workspace/graph-cli.d.ts +11 -5
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/intent-joins.d.ts +5 -5
- package/dist/workspace/kind-readers.d.ts +39 -0
- package/dist/workspace/kind-readers.d.ts.map +1 -0
- package/dist/workspace/kinds.d.ts +29 -0
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/member-commands.d.ts +15 -1
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/member-run.d.ts +2 -0
- package/dist/workspace/member-run.d.ts.map +1 -1
- package/dist/workspace/points-cli.d.ts +2 -0
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points.d.ts +3 -5
- package/dist/workspace/points.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +5 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +10 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +4 -2
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +8 -3
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +31 -9
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +20 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-evidence.d.ts +1 -1
- package/dist/workspace/work-evidence.d.ts.map +1 -1
- package/dist/workspace/workspace-kinds.schema.json +26 -0
- package/package.json +1 -1
- package/src/cli/commands/carve-bridge.test.ts +7 -3
- package/src/cli/handlers/operator-steward-signal.e2e.test.ts +97 -0
- package/src/cli/handlers/operator.ts +53 -11
- package/src/cli/handlers/run.test.ts +71 -0
- package/src/cli/handlers/run.ts +65 -7
- package/src/cli/main.ts +21 -9
- package/src/cli/mcp/workspace-plugins.ts +6 -6
- package/src/cli/mcp/workspace-tools.ts +1 -1
- package/src/cli/registry.ts +2 -0
- package/src/cli/serve-mcp-workspace.test.ts +6 -6
- package/src/cli/static-config-read.test.ts +8 -2
- package/src/meta/source-is-text.test.ts +21 -3
- package/src/okf.test.ts +6 -1
- package/src/op/activities/decide.test.ts +8 -0
- package/src/op/builders.ts +14 -3
- package/src/op/index.ts +8 -2
- package/src/op/operator.ts +264 -16
- package/src/op/steward-beside.test.ts +267 -0
- package/src/op/steward-beside.ts +219 -0
- package/src/op/steward-points.test.ts +112 -1
- package/src/op/steward.ts +135 -3
- package/src/workspace/box-intent.test.ts +205 -0
- package/src/workspace/box-intent.ts +159 -0
- package/src/workspace/box-services.test.ts +129 -0
- package/src/workspace/box-services.ts +51 -0
- package/src/workspace/chant-migrations.ts +5 -0
- package/src/workspace/check.schema.json +7 -3
- package/src/workspace/checks/boxes.test.ts +3 -1
- package/src/workspace/checks/boxes.ts +66 -0
- package/src/workspace/checks.ts +12 -1
- package/src/workspace/compose-graph.test.ts +1 -0
- package/src/workspace/compose-graph.ts +11 -0
- package/src/workspace/composites.test.ts +1 -1
- package/src/workspace/composites.ts +12 -5
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +4 -2
- package/src/workspace/decision-points.schema.json +3 -3
- package/src/workspace/declaration.schema.json +62 -1
- package/src/workspace/declaration.ts +112 -0
- package/src/workspace/declared-kinds.test.ts +26 -0
- package/src/workspace/graph-cache.test.ts +343 -0
- package/src/workspace/graph-cache.ts +409 -0
- package/src/workspace/graph-cli.ts +93 -26
- package/src/workspace/graph-contract.test.ts +129 -6
- package/src/workspace/graph.schema.json +21 -1
- package/src/workspace/intent-joins.test.ts +5 -5
- package/src/workspace/intent-joins.ts +5 -5
- package/src/workspace/kind-readers.e2e.test.ts +68 -0
- package/src/workspace/kind-readers.test.ts +134 -0
- package/src/workspace/kind-readers.ts +111 -0
- package/src/workspace/kinds.test.ts +49 -0
- package/src/workspace/kinds.ts +59 -2
- package/src/workspace/member-commands.test.ts +15 -0
- package/src/workspace/member-commands.ts +47 -7
- package/src/workspace/member-run.ts +10 -2
- package/src/workspace/points-cli.ts +3 -0
- package/src/workspace/points.schema.json +18 -0
- package/src/workspace/points.test.ts +15 -14
- package/src/workspace/points.ts +4 -16
- package/src/workspace/read-contract.test.ts +8 -0
- package/src/workspace/reason-codes.test.ts +7 -7
- package/src/workspace/reason-codes.ts +6 -1
- package/src/workspace/records-amend.schema.json +1 -0
- package/src/workspace/records-cli.ts +34 -13
- package/src/workspace/records-contract.test.ts +2 -1
- package/src/workspace/records-formats.test.ts +15 -15
- package/src/workspace/records-new.schema.json +1 -0
- package/src/workspace/records-quorum.test.ts +10 -3
- package/src/workspace/records-sessions-write.test.ts +2 -1
- package/src/workspace/records-since.test.ts +8 -7
- package/src/workspace/records-write.test.ts +101 -1
- package/src/workspace/records-write.ts +50 -6
- package/src/workspace/records.test.ts +1 -1
- package/src/workspace/records.ts +22 -5
- package/src/workspace/status-contract.test.ts +32 -1
- package/src/workspace/status-stewards.ts +88 -9
- package/src/workspace/status.schema.json +66 -4
- package/src/workspace/status.ts +32 -0
- package/src/workspace/trust/record-seal.test.ts +26 -2
- package/src/workspace/work-evidence.schema.json +1 -0
- package/src/workspace/{work-readiness-chud.test.ts → work-readiness.test.ts} +30 -32
- package/src/workspace/work.test.ts +17 -0
- package/src/workspace/workspace-kinds.schema.json +26 -0
|
@@ -37,11 +37,23 @@
|
|
|
37
37
|
* imports `Box` from `@intentius/chant-lexicon-fountain` and calls it, read as
|
|
38
38
|
* syntax and never run. Upstream, managoat/fountain#2497 asks for a way to run
|
|
39
39
|
* a persistent agent with no callback token.
|
|
40
|
+
*
|
|
41
|
+
* WSP126 (`box-intent-unknown`) and WSP127 (`box-intent-unconstrained`), #2850,
|
|
42
|
+
* read the decision record a box block names as its intent (`box-intent.ts`).
|
|
43
|
+
* WSP126 fails when no record of a declared kind named decision has the id.
|
|
44
|
+
* WSP127 warns when the record's constrains names no member or path of this
|
|
45
|
+
* workspace at all: no `member:` entry for a declared member and no `path:`
|
|
46
|
+
* entry at, above or inside one's directory. It need not reach the box's own
|
|
47
|
+
* member: the decision an intent names can constrain the member whose app the
|
|
48
|
+
* box runs, on a workspace where the box block sits on a different member,
|
|
49
|
+
* the box's steward (studio's template, #2857). Both read the working
|
|
50
|
+
* tree's records, so neither runs under `--at`.
|
|
40
51
|
*/
|
|
41
52
|
|
|
42
53
|
import * as ts from "typescript";
|
|
43
54
|
import type { WorkspaceCheck, WorkspaceCheckContext, WorkspaceDiagnostic } from "../checks";
|
|
44
55
|
import type { Member } from "../declaration";
|
|
56
|
+
import { constrainsWorkspace } from "../box-intent";
|
|
45
57
|
import type { ReasonCode } from "../reason-codes";
|
|
46
58
|
import { joinPath, skippedDir, type WorkspaceTree } from "../tree";
|
|
47
59
|
import { BOX_ISOLATION_CHECKS } from "./box-isolation";
|
|
@@ -53,11 +65,15 @@ export const BOX_FINDING_CODES = [
|
|
|
53
65
|
"box-isolation-collision",
|
|
54
66
|
"box-isolation-literal",
|
|
55
67
|
"box-fountain-callback-undeclared",
|
|
68
|
+
"box-intent-unknown",
|
|
69
|
+
"box-intent-unconstrained",
|
|
56
70
|
] as const satisfies readonly ReasonCode[];
|
|
57
71
|
|
|
58
72
|
export const WSP_BOX_CREDENTIAL = "WSP121";
|
|
59
73
|
export const WSP_BOX_UNBROKERED = "WSP122";
|
|
60
74
|
export const WSP_BOX_FOUNTAIN_CALLBACK = "WSP125";
|
|
75
|
+
export const WSP_BOX_INTENT_UNKNOWN = "WSP126";
|
|
76
|
+
export const WSP_BOX_INTENT_UNCONSTRAINED = "WSP127";
|
|
61
77
|
|
|
62
78
|
/** The fountain lexicon's package, whose `Box` composite runs a persistent sandbox. */
|
|
63
79
|
const FOUNTAIN_LEXICON = "@intentius/chant-lexicon-fountain";
|
|
@@ -439,4 +455,54 @@ export const BOX_CHECKS: readonly WorkspaceCheck[] = [
|
|
|
439
455
|
return out;
|
|
440
456
|
},
|
|
441
457
|
},
|
|
458
|
+
{
|
|
459
|
+
id: WSP_BOX_INTENT_UNKNOWN,
|
|
460
|
+
name: "box-intent-unknown",
|
|
461
|
+
description: "The intent a box block names is the id of a decision record: a record of a declared kind named decision.",
|
|
462
|
+
severity: "error",
|
|
463
|
+
configurable: true,
|
|
464
|
+
check(ctx) {
|
|
465
|
+
const out: WorkspaceDiagnostic[] = [];
|
|
466
|
+
for (const i of ctx.facts?.boxIntents ?? []) {
|
|
467
|
+
if (i.record) continue;
|
|
468
|
+
out.push({
|
|
469
|
+
checkId: this.id,
|
|
470
|
+
severity: this.severity,
|
|
471
|
+
code: "box-intent-unknown",
|
|
472
|
+
message: `box-intent-unknown: member ${i.member}'s box names the intent ${i.id}, and ${i.why}; propose the decision with chant workspace records new, or fix the id`,
|
|
473
|
+
entity: i.member,
|
|
474
|
+
pointer: i.pointer,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return out;
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
id: WSP_BOX_INTENT_UNCONSTRAINED,
|
|
482
|
+
name: "box-intent-unconstrained",
|
|
483
|
+
description:
|
|
484
|
+
"The decision record a box names as its intent constrains a member or path of this workspace: member:<a declared member's name>, or a path: entry at, above or inside a declared member's directory. It need not be the box's own member: a box one member runs can be what a decision about another member constrains.",
|
|
485
|
+
severity: "warning",
|
|
486
|
+
configurable: true,
|
|
487
|
+
check(ctx) {
|
|
488
|
+
const out: WorkspaceDiagnostic[] = [];
|
|
489
|
+
const members = ctx.declaration.members;
|
|
490
|
+
for (const i of ctx.facts?.boxIntents ?? []) {
|
|
491
|
+
if (!i.record) continue;
|
|
492
|
+
if (i.record.constrains.some((c) => constrainsWorkspace(c, members))) continue;
|
|
493
|
+
out.push({
|
|
494
|
+
checkId: this.id,
|
|
495
|
+
severity: this.severity,
|
|
496
|
+
code: "box-intent-unconstrained",
|
|
497
|
+
message:
|
|
498
|
+
`box-intent-unconstrained: member ${i.member}'s box names the intent ${i.id} (${i.record.path}), whose constrains ` +
|
|
499
|
+
(i.record.constrains.length === 0 ? "is empty" : `names ${i.record.constrains.join(", ")}`) +
|
|
500
|
+
`, and none of it is a member or path of this workspace; add member:<name> for the member the intent is about, or a path: entry at, above or inside a member's directory`,
|
|
501
|
+
entity: i.member,
|
|
502
|
+
pointer: i.pointer,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
return out;
|
|
506
|
+
},
|
|
507
|
+
},
|
|
442
508
|
];
|
package/src/workspace/checks.ts
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
* | WSP121, WSP122 | boxes: no literal credential, every capability brokered (#2726) |
|
|
34
34
|
* | WSP123, WSP124 | box isolation: no shared port, state path or cookie, no literal machine path (#2727) |
|
|
35
35
|
* | WSP125 | boxes: a fountain Box declares the callback token fountain gives its sandbox (#2780) |
|
|
36
|
+
* | WSP126, WSP127 | box intent: the decision record a box names exists, and constrains a member or path of this workspace (#2850, #2857) |
|
|
36
37
|
* | WSP131 to WSP133 | diagram artifacts: source and render exist, a recorded source hash still matches (#2764) |
|
|
37
38
|
*/
|
|
38
39
|
|
|
@@ -54,6 +55,7 @@ import { RECORD_CHECKS, type RecordFacts } from "./checks/records";
|
|
|
54
55
|
import { BOX_CHECKS } from "./checks/boxes";
|
|
55
56
|
import { DIAGRAM_CHECKS } from "./checks/diagrams";
|
|
56
57
|
import { loadDeclaredKinds, type DeclaredKind } from "./declared-kinds";
|
|
58
|
+
import { resolveBoxIntents, type ResolvedBoxIntent } from "./box-intent";
|
|
57
59
|
|
|
58
60
|
/**
|
|
59
61
|
* What the checks beyond the declaration read, gathered from the checkout
|
|
@@ -71,6 +73,8 @@ export interface WorkspaceFacts {
|
|
|
71
73
|
records?: RecordFacts;
|
|
72
74
|
/** The record kinds the declaration names, each loaded, or only looked for under `--at` (#2680). */
|
|
73
75
|
declaredKinds?: readonly DeclaredKind[];
|
|
76
|
+
/** The decision record each box's intent names, read from the working tree (#2850). */
|
|
77
|
+
boxIntents?: readonly ResolvedBoxIntent[];
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
/** What every declaration check reads. */
|
|
@@ -469,7 +473,14 @@ export async function runDeclarationChecks(
|
|
|
469
473
|
// The declared record kinds load from the working tree; under --at only whether each exists at the revision is checked (#2680).
|
|
470
474
|
// A declared work kind's acceptance criteria are counted in the working tree too (#2772).
|
|
471
475
|
const declaredKinds = options.gather === false ? undefined : await loadDeclaredKinds(declaration, tree, root, { load: !options.tree, acceptance: !options.tree });
|
|
472
|
-
|
|
476
|
+
// A box's intent is read from the working tree's records, so not under --at (#2850).
|
|
477
|
+
const boxIntents = declaredKinds && !options.tree ? await resolveBoxIntents(declaration, root, declaredKinds) : undefined;
|
|
478
|
+
const facts: WorkspaceFacts = {
|
|
479
|
+
...gathered,
|
|
480
|
+
...(options.records ? { records: options.records } : {}),
|
|
481
|
+
...(declaredKinds ? { declaredKinds } : {}),
|
|
482
|
+
...(boxIntents ? { boxIntents } : {}),
|
|
483
|
+
};
|
|
473
484
|
const ctx: WorkspaceCheckContext = { declaration, tree, groups, kinds: registry, kindProblems: problems, facts };
|
|
474
485
|
const findings = runWorkspaceChecks(ctx);
|
|
475
486
|
const { active, suppressed } = applyCheckSettings(declaration, findings);
|
|
@@ -76,6 +76,17 @@ export interface ComposedMember {
|
|
|
76
76
|
chant: string | null;
|
|
77
77
|
/** The IR version the member printed; `null` when it printed none and was upgraded as version 1. */
|
|
78
78
|
irVersion: number | null;
|
|
79
|
+
/** Whether the member was read with `--live` (#2875): its graph is the account as it stands, not its source. */
|
|
80
|
+
live: boolean;
|
|
81
|
+
/** For a live read, when the member's read finished, as an ISO time. */
|
|
82
|
+
readAt?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Whether the per-member cache answered this read (#2876). Set on composed
|
|
85
|
+
* members by `chant workspace graph`.
|
|
86
|
+
*/
|
|
87
|
+
cached?: boolean;
|
|
88
|
+
/** The member's stamp (#2876): what the cache keys the read on, or null when none could be taken. */
|
|
89
|
+
stamp?: string | null;
|
|
79
90
|
/** Whole-read facts the member's IR carried (`meta`, `pipeline`), kept apart from the composed sections. */
|
|
80
91
|
meta?: Record<string, unknown>;
|
|
81
92
|
pipeline?: unknown;
|
|
@@ -113,7 +113,7 @@ describe("composites output schema", () => {
|
|
|
113
113
|
test("lists exactly the reason and error codes the code can return", () => {
|
|
114
114
|
expect(schema.$defs.reason.properties.code.enum).toEqual([...COMPOSITES_REASON_CODES]);
|
|
115
115
|
expect(schema.$defs.failure.properties.error.properties.code.enum).toEqual([...COMPOSITES_ERROR_CODES]);
|
|
116
|
-
expect(COMPOSITES_ERROR_CODES).toEqual(GRAPH_ERROR_CODES);
|
|
116
|
+
expect(COMPOSITES_ERROR_CODES).toEqual(GRAPH_ERROR_CODES.filter((c) => c !== "live-at-revision"));
|
|
117
117
|
expect(schema.$defs.member.properties.reason.oneOf[1].properties!.code.enum).toEqual([...MEMBER_RUN_REASON_CODES]);
|
|
118
118
|
expect(schema.$defs.member.properties.runtimeReasons.items.properties.code.enum).toEqual([...COMPOSITES_RUNTIME_REASON_CODES]);
|
|
119
119
|
expect(schema.$defs.member.properties.environmentReasons.items.properties.code.enum).toEqual([...COMPOSITES_ENVIRONMENT_REASON_CODES]);
|
|
@@ -44,10 +44,10 @@ import type { CommandContext } from "../cli/registry";
|
|
|
44
44
|
import { joinLabel, type JoinLabel } from "../join-key";
|
|
45
45
|
import type { ComposedMember, MemberReason, WorkspaceGraph } from "./compose-graph";
|
|
46
46
|
import { readMemberIr } from "./compose-graph";
|
|
47
|
-
import {
|
|
47
|
+
import { workspaceGraph, type GraphQuery } from "./graph-cli";
|
|
48
48
|
import type { LinkRow } from "./links";
|
|
49
49
|
import { emitDocument, type UnitResult } from "./member-commands";
|
|
50
|
-
import { readerVersion, type ErrorLocation, type WorkspaceErrorCode } from "./declaration";
|
|
50
|
+
import { readerVersion, WORKSPACE_ERROR_CODES, type ErrorLocation, type WorkspaceErrorCode } from "./declaration";
|
|
51
51
|
import type { ReasonCode } from "./reason-codes";
|
|
52
52
|
import { componentEnvironments, ENVIRONMENT_REASON_CODES, memberEnvironments, readLedgerEnvironments, type ComponentEnvironment, type EnvironmentReason, type LedgerEnvironmentReader, type MemberEnvironments } from "./environments";
|
|
53
53
|
import { componentRuntimes, readRuntimesIn, RUNTIME_REASON_CODES, type ComponentRuntime, type MemberRuntimes, type PluginLoader, type RuntimeReason } from "./runtimes";
|
|
@@ -58,8 +58,12 @@ export const COMPOSITES_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/w
|
|
|
58
58
|
/** The read-contract version the document follows. */
|
|
59
59
|
export const COMPOSITES_CONTRACT_VERSION = 1;
|
|
60
60
|
|
|
61
|
-
/**
|
|
62
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Why the composites couldn't be read at all: the declaration's codes, as for
|
|
63
|
+
* the graph. The composites never read live, so the graph's own
|
|
64
|
+
* `live-at-revision` (#2875) can't reach them.
|
|
65
|
+
*/
|
|
66
|
+
export const COMPOSITES_ERROR_CODES = WORKSPACE_ERROR_CODES;
|
|
63
67
|
|
|
64
68
|
/** Why the list is empty, or why no instance has a component. Closed: part of the read contract. */
|
|
65
69
|
export const COMPOSITES_REASON_CODES = [
|
|
@@ -262,12 +266,15 @@ export async function workspaceComposites(
|
|
|
262
266
|
const { loadPlugin, readLedgerEnvironments: readLedger = readLedgerEnvironments, ...graphQuery } = query;
|
|
263
267
|
const { doc: graph, failed, components: runs } = await workspaceGraph({
|
|
264
268
|
...graphQuery,
|
|
269
|
+
// The composites read source only; a live flag never reaches the members.
|
|
270
|
+
...(graphQuery.args ? { args: { ...graphQuery.args, live: false, overlay: false, traffic: undefined } } : {}),
|
|
265
271
|
components: true,
|
|
266
272
|
inTree: async (root, members) => {
|
|
267
273
|
runtimes = await readRuntimesIn(root, members, loadPlugin);
|
|
268
274
|
},
|
|
269
275
|
});
|
|
270
|
-
|
|
276
|
+
// With no live read, the graph fails only with a declaration code.
|
|
277
|
+
if ("error" in graph) return { doc: { ...head, error: { ...graph.error, code: graph.error.code as WorkspaceErrorCode } }, failed: true };
|
|
271
278
|
|
|
272
279
|
const byMember = new Map((runs ?? []).map((r) => [r.unit.member, r]));
|
|
273
280
|
const members: CompositesMember[] = [];
|
|
@@ -33,8 +33,10 @@ export const recordKind = {
|
|
|
33
33
|
// (#2773): chant workspace check --changes reports change-out-of-scope.
|
|
34
34
|
outOfScope: { field: "out_of_scope" },
|
|
35
35
|
// Verdicts, and the field naming the decider, for each record's digest and
|
|
36
|
-
// quorum (#2671, #2672).
|
|
37
|
-
|
|
36
|
+
// quorum (#2671, #2672). A decision becomes ratified only once its quorum
|
|
37
|
+
// is met: records new and amend refuse it below, and the digest leaves the
|
|
38
|
+
// state out, so ratifying keeps the verdicts counting (#2873).
|
|
39
|
+
reviews: { field: "reviews", decider: "decided_by", ratified: "ratified" },
|
|
38
40
|
// records new --by and the MCP records-new tool's by name a proposal's
|
|
39
41
|
// proposer here, apart from decided_by, which stays null until the
|
|
40
42
|
// decision is decided (#2756).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://intentius.io/chant/schemas/workspace/decision-points/v1/decision-points.schema.json",
|
|
4
4
|
"title": "Decision points",
|
|
5
|
-
"description": "A workspace's decision points (ws-058, #2738): the recurring questions it asks of its own graph, declared as data in a JSON file that an answer record kind names in `answers.points`. Each point is a typed question (`noul`, `choice` with 2 to 255 options, or `score` with 2 to 10 ordered levels, the question types of the POST /v1/systemone wire format, #2491), the inputs it reads, each named as a read-contract output, and an ordered chain of deciders: `table` rows, `model` deciders with a backend, a pinned model id and a threshold, and a `quorum` of people, always last.
|
|
5
|
+
"description": "A workspace's decision points (ws-058, #2738): the recurring questions it asks of its own graph, declared as data in a JSON file that an answer record kind names in `answers.points`. Each point is a typed question (`noul`, `choice` with 2 to 255 options, or `score` with 2 to 10 ordered levels, the question types of the POST /v1/systemone wire format, #2491), the inputs it reads, each named as a read-contract output, and an ordered chain of deciders: `table` rows, `model` deciders with a backend, a pinned model id and a threshold, and a `quorum` of people, always last. What JSON Schema cannot say is checked in code (`points.ts`): an input's output is one the read contract has, a table row tests declared inputs and answers a candidate, a field belongs to its decider's kind, a model id is pinned rather than an alias, and the chain ends in its one quorum. Unknown fields are refused, except fields whose names start with x-.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["points"],
|
|
8
8
|
"properties": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"type": "object",
|
|
45
45
|
"required": ["type", "instructions", "criteria"],
|
|
46
46
|
"properties": {
|
|
47
|
-
"type": { "enum": ["noul", "
|
|
47
|
+
"type": { "enum": ["noul", "choice", "score"] },
|
|
48
48
|
"instructions": { "$ref": "#/$defs/text" },
|
|
49
49
|
"criteria": {
|
|
50
50
|
"description": "The candidates. noul: {true, false} descriptions. choice: option to description, 2 to 255 options. score: 2 to 10 ordered levels.",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"additionalProperties": false,
|
|
55
55
|
"allOf": [
|
|
56
56
|
{
|
|
57
|
-
"if": { "properties": { "type": { "
|
|
57
|
+
"if": { "properties": { "type": { "const": "noul" } } },
|
|
58
58
|
"then": {
|
|
59
59
|
"properties": {
|
|
60
60
|
"criteria": {
|
|
@@ -402,8 +402,20 @@
|
|
|
402
402
|
},
|
|
403
403
|
"box": {
|
|
404
404
|
"type": "object",
|
|
405
|
-
"description": "The member is a box, or holds a box's declarations (#2726). A box holds no credential: each capability it needs outside itself is reached through a broker, a runtime such as a lobby or a door, which holds the credential and enforces the scope. chant workspace check fails when a file in the member's directory carries a literal secret (WSP121) and when a capability names no broker (WSP122). chant workspace status --json lists the capabilities, so a broker can read the scopes. Its host, slot, ports, state and cookies state the box's isolation (#2727): chant derives each value from the box's identity, its host, the member's name and its slot, and prints them in chant workspace status --json, so a runtime reads them instead of choosing its own. chant workspace check fails when two boxes on a host resolve to the same value (WSP123) and when a box hard-codes a machine path (WSP124). A member that builds the fountain lexicon's Box declares the callback token fountain gives its persistent sandbox as the capability fountain-callback, brokered by fountain, with scope owner, or chant workspace check fails (WSP125, #2780). Added in schema 1 by chant 0.91.0, so a declaration that uses it sets minReader to 0.91.0 or newer.",
|
|
405
|
+
"description": "The member is a box, or holds a box's declarations (#2726). A box holds no credential: each capability it needs outside itself is reached through a broker, a runtime such as a lobby or a door, which holds the credential and enforces the scope. chant workspace check fails when a file in the member's directory carries a literal secret (WSP121) and when a capability names no broker (WSP122). chant workspace status --json lists the capabilities, so a broker can read the scopes. Its host, slot, ports, state and cookies state the box's isolation (#2727): chant derives each value from the box's identity, its host, the member's name and its slot, and prints them in chant workspace status --json, so a runtime reads them instead of choosing its own. chant workspace check fails when two boxes on a host resolve to the same value (WSP123) and when a box hard-codes a machine path (WSP124). A member that builds the fountain lexicon's Box declares the callback token fountain gives its persistent sandbox as the capability fountain-callback, brokered by fountain, with scope owner, or chant workspace check fails (WSP125, #2780). Its intent names the decision record that says what the box is for (#2850): chant workspace status --json reports the record's state and answer, and chant workspace check fails when no decision record has the id (WSP126) and warns when the record constrains no member or path of this workspace (WSP127, #2857). Its services list what the box runs under its supervisor (#2880), which the fly lexicon observes, restarts and applies through sprite-env, and which chant workspace status --json prints. Added in schema 1 by chant 0.91.0, so a declaration that uses it sets minReader to 0.91.0 or newer.",
|
|
406
406
|
"properties": {
|
|
407
|
+
"intent": {
|
|
408
|
+
"type": "string",
|
|
409
|
+
"minLength": 1,
|
|
410
|
+
"description": "The id of the decision record that says what the box is for (#2850), a record of a declared kind named decision. A new box starts as a question: the record is proposed with a null choice until the person who answers it decides it. The record constrains a member or path of this workspace, with member:<a member's name> or a path: entry at, above or inside a member's directory. It need not be this member: the intent can constrain the app the box runs when that app is another member (#2857). When it constrains this member, chant workspace graph --intent shows it for the box's files. Added by chant 0.94.0, so a declaration that uses it sets minReader to 0.94.0 or newer."
|
|
411
|
+
},
|
|
412
|
+
"services": {
|
|
413
|
+
"type": "array",
|
|
414
|
+
"description": "The services the box runs under its supervisor, sprite-env on a sprite (#2880). Each name is given once, every needs entry names a service of this list, the needs form no cycle, and at most one service sets httpPort; a declaration that breaks one of these can't be read (declaration-invalid). The fly lexicon's spriteServicesObserve and spriteServiceRestart read the list with box: true, and spriteApplyServices with box: true creates and replaces the services through sprite-env. None when omitted. Added by chant 0.95.0, so a declaration that uses it sets minReader to 0.95.0 or newer.",
|
|
415
|
+
"items": {
|
|
416
|
+
"$ref": "#/$defs/boxService"
|
|
417
|
+
}
|
|
418
|
+
},
|
|
407
419
|
"capabilities": {
|
|
408
420
|
"type": "array",
|
|
409
421
|
"description": "Each capability the box reaches outside itself, named once. None when omitted.",
|
|
@@ -476,6 +488,55 @@
|
|
|
476
488
|
},
|
|
477
489
|
"additionalProperties": false
|
|
478
490
|
},
|
|
491
|
+
"boxService": {
|
|
492
|
+
"type": "object",
|
|
493
|
+
"description": "A service a box runs under its supervisor (#2880).",
|
|
494
|
+
"required": [
|
|
495
|
+
"name",
|
|
496
|
+
"cmd"
|
|
497
|
+
],
|
|
498
|
+
"properties": {
|
|
499
|
+
"name": {
|
|
500
|
+
"$ref": "#/$defs/kindName",
|
|
501
|
+
"description": "The service's name in the supervisor. Unique within the box."
|
|
502
|
+
},
|
|
503
|
+
"cmd": {
|
|
504
|
+
"type": "string",
|
|
505
|
+
"minLength": 1,
|
|
506
|
+
"description": "The command the supervisor runs (sprite-env services create --cmd). ${HOME} and other ${VAR} references are expanded from the environment of the process that applies the list, so the declaration holds no machine path."
|
|
507
|
+
},
|
|
508
|
+
"needs": {
|
|
509
|
+
"type": "array",
|
|
510
|
+
"uniqueItems": true,
|
|
511
|
+
"description": "Names of services of the same box that start before this one (sprite-env services create --needs).",
|
|
512
|
+
"items": {
|
|
513
|
+
"$ref": "#/$defs/kindName"
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
"httpPort": {
|
|
517
|
+
"$ref": "#/$defs/port",
|
|
518
|
+
"description": "The port the supervisor routes the sprite's URL to (sprite-env services create --http-port). At most one service of a box sets it."
|
|
519
|
+
},
|
|
520
|
+
"duration": {
|
|
521
|
+
"type": "string",
|
|
522
|
+
"pattern": "^[0-9]+(\\.[0-9]+)?(ms|s|m)$",
|
|
523
|
+
"description": "How long the service must stay up after it is created or started, such as 3s (sprite-env services create --duration)."
|
|
524
|
+
},
|
|
525
|
+
"health": {
|
|
526
|
+
"type": "string",
|
|
527
|
+
"pattern": "^https?://",
|
|
528
|
+
"description": "A URL that answers 200 while the service works. spriteServicesObserve probes it, and spriteServiceRestart waits for it after a restart. Without it, the supervisor's state decides."
|
|
529
|
+
},
|
|
530
|
+
"optional": {
|
|
531
|
+
"type": "boolean",
|
|
532
|
+
"description": "True for a service something else creates by name, such as a site a release Op makes: spriteApplyServices creates it only when its only names it, and spriteServicesObserve skips it while the supervisor has no such service."
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
"patternProperties": {
|
|
536
|
+
"^x-": true
|
|
537
|
+
},
|
|
538
|
+
"additionalProperties": false
|
|
539
|
+
},
|
|
479
540
|
"capability": {
|
|
480
541
|
"type": "object",
|
|
481
542
|
"description": "A capability a box needs and does not hold the credential for, such as inference, fountain or a third-party API (#2726).",
|
|
@@ -210,10 +210,43 @@ export interface BoxDeclaration {
|
|
|
210
210
|
capabilities: BoxCapability[];
|
|
211
211
|
/** The host and slot of the box's isolation, or null when the block declares none (#2727). The values are derived in `boxes.ts`. */
|
|
212
212
|
isolation: BoxIsolationDeclaration | null;
|
|
213
|
+
/**
|
|
214
|
+
* The id of the decision record that says what the box is for, or null when
|
|
215
|
+
* the block names none (#2850). A box starts as a question: the record is
|
|
216
|
+
* proposed with no choice until the person who answers it decides it.
|
|
217
|
+
*/
|
|
218
|
+
intent: string | null;
|
|
219
|
+
/**
|
|
220
|
+
* The services the box runs under its supervisor (sprite-env on a sprite),
|
|
221
|
+
* in file order, or empty when the block declares none (#2880). The fly
|
|
222
|
+
* lexicon's `spriteServicesObserve`, `spriteServiceRestart` and
|
|
223
|
+
* `spriteApplyServices` read them with `box: true`.
|
|
224
|
+
*/
|
|
225
|
+
services: BoxService[];
|
|
213
226
|
/** The block's JSON Pointer in the file, for messages. */
|
|
214
227
|
pointer: string;
|
|
215
228
|
}
|
|
216
229
|
|
|
230
|
+
/** A service a box runs under its supervisor (#2880). */
|
|
231
|
+
export interface BoxService {
|
|
232
|
+
/** Unique in the box. */
|
|
233
|
+
name: string;
|
|
234
|
+
/** The command the supervisor runs, as written: `${VAR}` references are left for the applying process to expand. */
|
|
235
|
+
cmd: string;
|
|
236
|
+
/** Names of services in the same block that start first. Empty when none. */
|
|
237
|
+
needs: string[];
|
|
238
|
+
/** The port the supervisor routes the sprite's URL to, or null. At most one service of a box sets it. */
|
|
239
|
+
httpPort: number | null;
|
|
240
|
+
/** How long the service must stay up after a create or start, such as `3s` (`sprite-env services create --duration`), or null for the supervisor's default. */
|
|
241
|
+
duration: string | null;
|
|
242
|
+
/** A URL that answers 200 while the service works, or null when the supervisor's state decides. */
|
|
243
|
+
health: string | null;
|
|
244
|
+
/** True: an apply creates it only when named, and an observer skips it while the supervisor has no such service. */
|
|
245
|
+
optional: boolean;
|
|
246
|
+
/** The entry's JSON Pointer in the file, for messages. */
|
|
247
|
+
pointer: string;
|
|
248
|
+
}
|
|
249
|
+
|
|
217
250
|
/** What a box declares about its isolation (#2727): its identity on a host, and the names of what it needs kept apart. */
|
|
218
251
|
export interface BoxIsolationDeclaration {
|
|
219
252
|
host: string;
|
|
@@ -623,6 +656,9 @@ export function parseDeclaration(text: string, file: string, reader: string = re
|
|
|
623
656
|
if (first) throw new WorkspaceReadError("declaration-invalid", `member ${m.name}'s box lists the capability ${c.name} twice; the first is at ${first.pointer}`, at(`${c.pointer}/name`));
|
|
624
657
|
seen.set(c.name, c);
|
|
625
658
|
}
|
|
659
|
+
// Its services name each other only within the block, without a cycle (#2880).
|
|
660
|
+
const problem = m.box ? boxServicesProblem(m.name, m.box.services) : null;
|
|
661
|
+
if (problem) throw new WorkspaceReadError("declaration-invalid", problem.message, at(problem.pointer));
|
|
626
662
|
}
|
|
627
663
|
|
|
628
664
|
// A diagram name is given once across the declaration (#2764): a reader keys diagrams by name.
|
|
@@ -713,6 +749,8 @@ function boxOf(raw: unknown, pointer: string): BoxDeclaration | null {
|
|
|
713
749
|
ports?: Record<string, number>;
|
|
714
750
|
state?: Record<string, string>;
|
|
715
751
|
cookies?: string[];
|
|
752
|
+
intent?: string;
|
|
753
|
+
services?: { name: string; cmd: string; needs?: string[]; httpPort?: number; duration?: string; health?: string; optional?: boolean }[];
|
|
716
754
|
};
|
|
717
755
|
return {
|
|
718
756
|
capabilities: (b.capabilities ?? []).map((c, i) => ({ name: c.name, broker: c.broker ?? null, scope: [...(c.scope ?? [])], pointer: `${pointer}/capabilities/${i}` })),
|
|
@@ -721,10 +759,84 @@ function boxOf(raw: unknown, pointer: string): BoxDeclaration | null {
|
|
|
721
759
|
b.host === undefined
|
|
722
760
|
? null
|
|
723
761
|
: { host: b.host, slot: b.slot!, ports: { ...(b.ports ?? {}) }, state: { ...(b.state ?? {}) }, cookies: [...(b.cookies ?? [])] },
|
|
762
|
+
intent: b.intent ?? null,
|
|
763
|
+
services: (b.services ?? []).map((s, i) => ({
|
|
764
|
+
name: s.name,
|
|
765
|
+
cmd: s.cmd,
|
|
766
|
+
needs: [...(s.needs ?? [])],
|
|
767
|
+
httpPort: s.httpPort ?? null,
|
|
768
|
+
duration: s.duration ?? null,
|
|
769
|
+
health: s.health ?? null,
|
|
770
|
+
optional: s.optional ?? false,
|
|
771
|
+
pointer: `${pointer}/services/${i}`,
|
|
772
|
+
})),
|
|
724
773
|
pointer,
|
|
725
774
|
};
|
|
726
775
|
}
|
|
727
776
|
|
|
777
|
+
/**
|
|
778
|
+
* The rules the schema can't say about a box's services (#2880), as a
|
|
779
|
+
* message and the pointer to show, or null when they hold: each name is
|
|
780
|
+
* given once, every `needs` names a service of the same block, the `needs`
|
|
781
|
+
* form no cycle, and at most one service sets `httpPort`. The fly lexicon
|
|
782
|
+
* checks a list it is handed inline with the same rules.
|
|
783
|
+
*/
|
|
784
|
+
export function boxServicesProblem(member: string, services: readonly BoxService[]): { message: string; pointer: string } | null {
|
|
785
|
+
const byName = new Map<string, BoxService>();
|
|
786
|
+
for (const s of services) {
|
|
787
|
+
const first = byName.get(s.name);
|
|
788
|
+
if (first) return { message: `member ${member}'s box declares the service ${s.name} twice; the first is at ${first.pointer}`, pointer: `${s.pointer}/name` };
|
|
789
|
+
byName.set(s.name, s);
|
|
790
|
+
}
|
|
791
|
+
let routed: BoxService | undefined;
|
|
792
|
+
for (const s of services) {
|
|
793
|
+
if (s.httpPort === null) continue;
|
|
794
|
+
if (routed) {
|
|
795
|
+
return {
|
|
796
|
+
message: `member ${member}'s box gives both ${routed.name} (port ${routed.httpPort}) and ${s.name} (port ${s.httpPort}) an httpPort, and the supervisor routes the sprite's URL to one service`,
|
|
797
|
+
pointer: `${s.pointer}/httpPort`,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
routed = s;
|
|
801
|
+
}
|
|
802
|
+
for (const s of services) {
|
|
803
|
+
const j = s.needs.findIndex((n) => !byName.has(n));
|
|
804
|
+
if (j >= 0) {
|
|
805
|
+
const known = services.map((x) => x.name).join(", ");
|
|
806
|
+
return { message: `member ${member}'s box service ${s.name} needs ${JSON.stringify(s.needs[j])}, which the block does not declare; declared services: ${known}`, pointer: `${s.pointer}/needs/${j}` };
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const cycle = serviceCycle(services);
|
|
810
|
+
if (cycle) {
|
|
811
|
+
const s = byName.get(cycle[0])!;
|
|
812
|
+
return { message: `member ${member}'s box services need each other in a cycle: ${cycle.join(" -> ")}`, pointer: `${s.pointer}/needs` };
|
|
813
|
+
}
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** The first cycle the services' `needs` form, as names ending where it starts, or null. */
|
|
818
|
+
function serviceCycle(services: readonly { name: string; needs: readonly string[] }[]): string[] | null {
|
|
819
|
+
const byName = new Map(services.map((s) => [s.name, s]));
|
|
820
|
+
const state = new Map<string, "visiting" | "done">();
|
|
821
|
+
const visit = (name: string, trail: string[]): string[] | null => {
|
|
822
|
+
const st = state.get(name);
|
|
823
|
+
if (st === "done") return null;
|
|
824
|
+
if (st === "visiting") return [...trail.slice(trail.indexOf(name)), name];
|
|
825
|
+
state.set(name, "visiting");
|
|
826
|
+
for (const dep of byName.get(name)?.needs ?? []) {
|
|
827
|
+
const found = visit(dep, [...trail, name]);
|
|
828
|
+
if (found) return found;
|
|
829
|
+
}
|
|
830
|
+
state.set(name, "done");
|
|
831
|
+
return null;
|
|
832
|
+
};
|
|
833
|
+
for (const s of services) {
|
|
834
|
+
const found = visit(s.name, []);
|
|
835
|
+
if (found) return found;
|
|
836
|
+
}
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
|
|
728
840
|
/**
|
|
729
841
|
* The hosts, already validated, with the rules the schema can't say (#2727):
|
|
730
842
|
* host names are unique, a box's host is declared, its slot's block fits the
|
|
@@ -242,6 +242,32 @@ describe("records without --kind reads every declared kind (#2680)", () => {
|
|
|
242
242
|
expect(await run(workspace(), "--current")).toBe(0);
|
|
243
243
|
expect(out.filter((l) => l.includes("("))).toEqual(["decision (decisions/decision.kind.mjs)", "notes (design/notes/note.kind.mjs)"]);
|
|
244
244
|
});
|
|
245
|
+
|
|
246
|
+
test("--at an unknown revision with --json prints the error document, not just text on stderr (#2860)", async () => {
|
|
247
|
+
const root = workspace();
|
|
248
|
+
expect(await run(root, "--at", "refs/heads/no-such-branch", "--json")).toBe(1);
|
|
249
|
+
expect(err).toEqual([]);
|
|
250
|
+
const printed = JSON.parse(out.join("\n"));
|
|
251
|
+
expectValid(printed);
|
|
252
|
+
expect(printed).toEqual({
|
|
253
|
+
$schema: recordsSchema.$id,
|
|
254
|
+
contract: 1,
|
|
255
|
+
error: { code: "revision-unknown", message: expect.stringContaining("refs/heads/no-such-branch") as unknown as string },
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
out.length = 0;
|
|
259
|
+
err.length = 0;
|
|
260
|
+
expect(await run(root, "--at", "refs/heads/no-such-branch")).toBe(1);
|
|
261
|
+
expect(out).toEqual([]);
|
|
262
|
+
expect(err.join("\n")).toContain("revision-unknown");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("a declaration-level code the records schema doesn't carry, such as declaration-invalid, stays text-only even with --json (#2860)", async () => {
|
|
266
|
+
const root = repo({ "chant.workspace.json": decl(members(), { records: [{ kind: "a.kind.mjs", file: "b" }] }), "app/x": "", "design/x": "" });
|
|
267
|
+
expect(await run(root, "--json")).toBe(1);
|
|
268
|
+
expect(out).toEqual([]);
|
|
269
|
+
expect(err.join("\n")).toContain("declaration-invalid");
|
|
270
|
+
});
|
|
245
271
|
});
|
|
246
272
|
|
|
247
273
|
describe("records new, amend and review without --kind (#2680)", () => {
|