@intentius/chant 0.85.0 → 0.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +11 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +13 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/sessions.d.ts +23 -0
- package/dist/workspace/__fixtures__/sessions.d.ts.map +1 -0
- package/dist/workspace/checks/records.d.ts +1 -0
- package/dist/workspace/checks/records.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/composites.d.ts +14 -1
- package/dist/workspace/composites.d.ts.map +1 -1
- package/dist/workspace/conformance/index.d.ts +211 -0
- package/dist/workspace/conformance/index.d.ts.map +1 -0
- package/dist/workspace/conformance/vitest.d.ts +11 -0
- package/dist/workspace/conformance/vitest.d.ts.map +1 -0
- package/dist/workspace/declaration.d.ts +28 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +40 -0
- package/dist/workspace/declared-kinds.d.ts +43 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -0
- package/dist/workspace/graph-cli.d.ts +11 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/intent-cli.d.ts +2 -1
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-joins.d.ts +45 -8
- package/dist/workspace/intent-joins.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts +66 -7
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/ls.d.ts +31 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +40 -4
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-sessions.d.ts +51 -0
- package/dist/workspace/record-sessions.d.ts.map +1 -0
- package/dist/workspace/record-source.d.ts +2 -0
- package/dist/workspace/record-source.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +65 -4
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-since.d.ts +90 -0
- package/dist/workspace/records-since.d.ts.map +1 -0
- package/dist/workspace/records-write.d.ts +164 -0
- package/dist/workspace/records-write.d.ts.map +1 -0
- package/dist/workspace/records.d.ts +202 -15
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/runtimes.d.ts +60 -0
- package/dist/workspace/runtimes.d.ts.map +1 -0
- package/dist/workspace/status-gates.d.ts +90 -0
- package/dist/workspace/status-gates.d.ts.map +1 -0
- package/dist/workspace/status.d.ts +17 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work.d.ts +56 -0
- package/dist/workspace/work.d.ts.map +1 -0
- package/package.json +19 -1
- package/src/cli/main.ts +48 -3
- package/src/cli/registry.ts +11 -1
- package/src/lifecycle/gate-ledger.ts +14 -0
- package/src/workspace/__fixtures__/sessions.ts +66 -0
- package/src/workspace/checks/records.ts +19 -0
- package/src/workspace/checks.test.ts +2 -0
- package/src/workspace/checks.ts +7 -1
- package/src/workspace/composites.schema.json +65 -3
- package/src/workspace/composites.test.ts +95 -5
- package/src/workspace/composites.ts +28 -7
- package/src/workspace/conformance/__fixture__/app/package.json +7 -0
- package/src/workspace/conformance/__fixture__/app/src/server.mjs +29 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +32 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +364 -0
- package/src/workspace/conformance/__fixture__/decisions/fix-001-how-the-app-is-deployed.md +40 -0
- package/src/workspace/conformance/__fixture__/delivery/chant.config.ts +7 -0
- package/src/workspace/conformance/__fixture__/delivery/lexicon/index.ts +26 -0
- package/src/workspace/conformance/__fixture__/delivery/package.json +7 -0
- package/src/workspace/conformance/__fixture__/delivery/src/app.component.ts +14 -0
- package/src/workspace/conformance/__fixture__/delivery/src/app.ts +4 -0
- package/src/workspace/conformance/conformance.test.ts +149 -0
- package/src/workspace/conformance/index.mjs +31 -0
- package/src/workspace/conformance/index.ts +453 -0
- package/src/workspace/conformance/vitest.ts +62 -0
- package/src/workspace/declaration.schema.json +40 -0
- package/src/workspace/declaration.ts +62 -0
- package/src/workspace/declared-kinds.test.ts +321 -0
- package/src/workspace/declared-kinds.ts +76 -0
- package/src/workspace/graph-cli.ts +8 -0
- package/src/workspace/intent-cli.ts +29 -6
- package/src/workspace/intent-joins.test.ts +60 -0
- package/src/workspace/intent-joins.ts +71 -19
- package/src/workspace/intent.schema.json +282 -7
- package/src/workspace/intent.test.ts +97 -0
- package/src/workspace/intent.ts +332 -45
- package/src/workspace/ls.schema.json +34 -0
- package/src/workspace/ls.ts +69 -4
- package/src/workspace/read-contract.test.ts +30 -9
- package/src/workspace/reason-codes.test.ts +15 -4
- package/src/workspace/reason-codes.ts +47 -4
- package/src/workspace/record-assets.test.ts +3 -1
- package/src/workspace/record-sessions.ts +105 -0
- package/src/workspace/record-source.ts +14 -5
- package/src/workspace/records-amend.schema.json +167 -0
- package/src/workspace/records-cli.ts +246 -19
- package/src/workspace/records-contract.test.ts +57 -2
- package/src/workspace/records-formats.test.ts +640 -0
- package/src/workspace/records-new.schema.json +158 -0
- package/src/workspace/records-quorum.test.ts +196 -0
- package/src/workspace/records-review.schema.json +202 -0
- package/src/workspace/records-sessions.test.ts +108 -0
- package/src/workspace/records-since.schema.json +193 -0
- package/src/workspace/records-since.test.ts +174 -0
- package/src/workspace/records-since.ts +259 -0
- package/src/workspace/records-write-contract.test.ts +125 -0
- package/src/workspace/records-write.test.ts +373 -0
- package/src/workspace/records-write.ts +736 -0
- package/src/workspace/records.schema.json +187 -9
- package/src/workspace/records.ts +631 -41
- package/src/workspace/runtimes.ts +107 -0
- package/src/workspace/status-contract.test.ts +163 -0
- package/src/workspace/status-gates.ts +215 -0
- package/src/workspace/status.schema.json +69 -3
- package/src/workspace/status.ts +35 -2
- package/src/workspace/work.test.ts +388 -0
- package/src/workspace/work.ts +163 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work items on read (#2683): a record kind with a `work` block.
|
|
3
|
+
*
|
|
4
|
+
* A work item is a record in the workspace, a git-tracked file with its
|
|
5
|
+
* dependencies written inside it, that people and agents share as one queue
|
|
6
|
+
* with no server. Most work items come from a gap the intent graph already
|
|
7
|
+
* reports (`source.finding`), given an id, an owner and a lifecycle.
|
|
8
|
+
*
|
|
9
|
+
* `records.ts` calls {@link applyWork} after it has read a work kind's
|
|
10
|
+
* records, before it sets `valid` and applies `--current`. This module reads
|
|
11
|
+
* the decision kind the work kind names, at the same revision, and gives each
|
|
12
|
+
* work record:
|
|
13
|
+
*
|
|
14
|
+
* - `ready`: its state is the kind's open state, every need is done, and the
|
|
15
|
+
* record is valid and not superseded;
|
|
16
|
+
* - `blockedBy`: each need that is not done, with its state;
|
|
17
|
+
* - `implements`: each decision it names, with that decision's state;
|
|
18
|
+
*
|
|
19
|
+
* and each decision `implementedBy`, the work records naming it. The
|
|
20
|
+
* warnings are closed codes. `work-done-gap-open` is not raised here: only
|
|
21
|
+
* `graph --intent` walks a region, so only it can tell whether the finding a
|
|
22
|
+
* done item came from still fires. It never writes a record.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { dirname } from "node:path";
|
|
26
|
+
import type { ReasonCode } from "./reason-codes";
|
|
27
|
+
import { loadRecordKind, readRecords, type LoadedRecordKind, type ReadRecordsOptions, type RecordEntry } from "./records";
|
|
28
|
+
|
|
29
|
+
/** Why a work record carries a warning. Closed, like the record warning codes. */
|
|
30
|
+
export const WORK_WARNING_CODES = [
|
|
31
|
+
/** A `needs` entry names a work id no record has. */
|
|
32
|
+
"work-needs-unknown",
|
|
33
|
+
/** An `implements` entry names a decision id no decision has. */
|
|
34
|
+
"work-implements-unknown",
|
|
35
|
+
/** The record needs itself through its `needs` links, so it can never be ready. */
|
|
36
|
+
"work-needs-cycle",
|
|
37
|
+
/** The record implements a decision whose state is not approved, such as proposed or withdrawn. */
|
|
38
|
+
"work-implements-undecided",
|
|
39
|
+
/** The record is done and its evidence list is empty: nothing shows the work was done. */
|
|
40
|
+
"work-done-unpinned",
|
|
41
|
+
/** The record is in a closed state and has no closing date. */
|
|
42
|
+
"work-closed-without-date",
|
|
43
|
+
/** The record is done, and the finding it came from (`source.finding`) still fires on its region. Raised by `graph --intent` only. */
|
|
44
|
+
"work-done-gap-open",
|
|
45
|
+
] as const satisfies readonly ReasonCode[];
|
|
46
|
+
export type WorkWarningCode = (typeof WORK_WARNING_CODES)[number];
|
|
47
|
+
|
|
48
|
+
/** A link from a work record to another record, with that record's state now. */
|
|
49
|
+
export interface WorkLink {
|
|
50
|
+
id: string;
|
|
51
|
+
/** The linked record's state, or null when no record has the id. */
|
|
52
|
+
state: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A decision as a work read lists it: its state and the work records implementing it. */
|
|
56
|
+
export interface DecisionWork {
|
|
57
|
+
id: string;
|
|
58
|
+
path: string;
|
|
59
|
+
state: string | null;
|
|
60
|
+
supersededBy: string | null;
|
|
61
|
+
implementedBy: WorkLink[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The strings of a front-matter list, or none. */
|
|
65
|
+
export function idList(data: Record<string, unknown> | null, field: string): string[] {
|
|
66
|
+
const v = data?.[field];
|
|
67
|
+
return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether `state` counts as decided in the decision kind: ranked above 0 by
|
|
72
|
+
* its approval ranks, or, for a kind without them, closed.
|
|
73
|
+
*/
|
|
74
|
+
export function isDecided(kind: LoadedRecordKind["kind"], state: string | null): boolean {
|
|
75
|
+
if (state === null) return false;
|
|
76
|
+
return kind.approval ? (kind.approval[state] ?? 0) > 0 : (kind.closedStates ?? []).includes(state);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Give each work record its links, ready and blockedBy, and its warnings, in
|
|
81
|
+
* place. Throws a `RecordReadError` when the decision kind can't be read.
|
|
82
|
+
*/
|
|
83
|
+
export async function applyWork(loaded: LoadedRecordKind, entries: RecordEntry[], options: ReadRecordsOptions): Promise<{ decisions: DecisionWork[] }> {
|
|
84
|
+
const { kind } = loaded;
|
|
85
|
+
const work = kind.work!;
|
|
86
|
+
const decisionsKind = await loadRecordKind(work.decisions, dirname(loaded.file));
|
|
87
|
+
const decisionRead = await readRecords(decisionsKind, { root: options.root, source: options.source });
|
|
88
|
+
const decisionById = new Map<string, RecordEntry>();
|
|
89
|
+
for (const d of decisionRead.records) if (d.id !== null && !decisionById.has(d.id)) decisionById.set(d.id, d);
|
|
90
|
+
|
|
91
|
+
const byId = new Map<string, RecordEntry>();
|
|
92
|
+
for (const e of entries) if (e.id !== null && !byId.has(e.id)) byId.set(e.id, e);
|
|
93
|
+
const closed = new Set(kind.closedStates);
|
|
94
|
+
|
|
95
|
+
// Records in a needs cycle: those that reach themselves.
|
|
96
|
+
const inCycle = new Set<string>();
|
|
97
|
+
for (const start of byId.keys()) {
|
|
98
|
+
const seen = new Set<string>();
|
|
99
|
+
const stack = [...idList(byId.get(start)!.data, work.needs)];
|
|
100
|
+
while (stack.length > 0) {
|
|
101
|
+
const id = stack.pop()!;
|
|
102
|
+
if (id === start) {
|
|
103
|
+
inCycle.add(start);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
if (seen.has(id)) continue;
|
|
107
|
+
seen.add(id);
|
|
108
|
+
stack.push(...idList(byId.get(id)?.data ?? null, work.needs));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const implementedBy = new Map<string, WorkLink[]>();
|
|
113
|
+
for (const e of entries) {
|
|
114
|
+
if (e.data === null) continue;
|
|
115
|
+
const needs = idList(e.data, work.needs);
|
|
116
|
+
const implementsIds = idList(e.data, work.implements);
|
|
117
|
+
e.blockedBy = needs.filter((n) => byId.get(n)?.state !== work.done).map((n) => ({ id: n, state: byId.get(n)?.state ?? null }));
|
|
118
|
+
e.implements = implementsIds.map((d) => ({ id: d, state: decisionById.get(d)?.state ?? null }));
|
|
119
|
+
e.ready = e.state === work.open && e.blockedBy.length === 0 && e.reasons.length === 0 && e.supersededBy === null && !(e.id !== null && inCycle.has(e.id));
|
|
120
|
+
|
|
121
|
+
// An open item has no proof yet, so the kind's empty-evidence warning is
|
|
122
|
+
// replaced by work-done-unpinned, which only a done item gets.
|
|
123
|
+
e.warnings = e.warnings.filter((w) => w.code !== "record-no-evidence");
|
|
124
|
+
const unknownNeeds = needs.filter((n) => !byId.has(n));
|
|
125
|
+
if (unknownNeeds.length > 0) {
|
|
126
|
+
e.warnings.push({ code: "work-needs-unknown", message: `needs ${unknownNeeds.join(", ")}, which no work record has, so the item stays blocked` });
|
|
127
|
+
}
|
|
128
|
+
const unknownDecisions = implementsIds.filter((d) => !decisionById.has(d));
|
|
129
|
+
if (unknownDecisions.length > 0) {
|
|
130
|
+
e.warnings.push({ code: "work-implements-unknown", message: `implements ${unknownDecisions.join(", ")}, which no decision in ${work.decisions} has` });
|
|
131
|
+
}
|
|
132
|
+
if (e.id !== null && inCycle.has(e.id)) {
|
|
133
|
+
e.warnings.push({ code: "work-needs-cycle", message: `${e.id} needs itself through its needs links, so it can never be ready` });
|
|
134
|
+
}
|
|
135
|
+
const undecided = e.implements.filter((d) => d.state !== null && !isDecided(decisionsKind.kind, d.state));
|
|
136
|
+
if (undecided.length > 0) {
|
|
137
|
+
e.warnings.push({
|
|
138
|
+
code: "work-implements-undecided",
|
|
139
|
+
message: `implements ${undecided.map((d) => `${d.id}, which is ${d.state}`).join("; ")}: the work may carry out a choice nobody has made`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const evidence = kind.pins ? e.data[kind.pins.field] : undefined;
|
|
143
|
+
if (e.state === work.done && kind.pins && !(Array.isArray(evidence) && evidence.length > 0)) {
|
|
144
|
+
e.warnings.push({ code: "work-done-unpinned", message: `${e.id ?? e.path} is ${work.done} and ${kind.pins.field} is empty or missing: nothing shows the work was done` });
|
|
145
|
+
}
|
|
146
|
+
if (e.state !== null && closed.has(e.state) && typeof e.data[work.closedOn] !== "string") {
|
|
147
|
+
e.warnings.push({ code: "work-closed-without-date", message: `${e.id ?? e.path} is ${e.state} and has no ${work.closedOn}` });
|
|
148
|
+
}
|
|
149
|
+
if (e.id !== null && byId.get(e.id) === e) {
|
|
150
|
+
for (const d of implementsIds) {
|
|
151
|
+
const list = implementedBy.get(d) ?? [];
|
|
152
|
+
if (!list.some((x) => x.id === e.id)) list.push({ id: e.id, state: e.state });
|
|
153
|
+
implementedBy.set(d, list);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
decisions: decisionRead.records
|
|
160
|
+
.filter((d): d is RecordEntry & { id: string } => d.id !== null && decisionById.get(d.id) === d)
|
|
161
|
+
.map((d) => ({ id: d.id, path: d.path, state: d.state, supersededBy: d.supersededBy, implementedBy: implementedBy.get(d.id) ?? [] })),
|
|
162
|
+
};
|
|
163
|
+
}
|