@voltro/plan 0.85.0 → 0.87.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/lint.js ADDED
@@ -0,0 +1,273 @@
1
+ import { ownerItemOf as e, planNumberOf as t } from "./ids.js";
2
+ import { evidenceSchema as n, findingSchema as r, planInstanceSchema as i } from "./schema.js";
3
+ import { sortDiagnostics as a } from "./diagnostics.js";
4
+ import { DERIVED as o, TRANSITIONS as s, derivedStatus as c, indexEvidence as l, verificationGaps as u } from "./state.js";
5
+ import { closureFileSchema as d, closureState as f, planRevision as p } from "./closure.js";
6
+ import { Cause as m, Exit as h, Schema as g, SchemaIssue as _ } from "effect";
7
+ //#region src/lint.ts
8
+ var v = (e, t, n, r, i, a = []) => ({
9
+ ruleId: e,
10
+ severity: t,
11
+ message: n,
12
+ locator: r,
13
+ related: [...a].sort(),
14
+ nextAction: i
15
+ }), y = _.makeFormatterStandardSchemaV1(), b = (e, t, n, r) => {
16
+ let i = g.decodeUnknownExit(t, {
17
+ onExcessProperty: "error",
18
+ errors: "all"
19
+ }), a = [];
20
+ for (let t of e) {
21
+ let e = i(t.content);
22
+ if (h.isSuccess(e)) {
23
+ a.push({
24
+ file: t.file,
25
+ value: e.value
26
+ });
27
+ continue;
28
+ }
29
+ let o = m.findErrorOption(e.cause), s = o._tag === "Some" ? y(o.value.issue).issues : [{
30
+ message: m.pretty(e.cause),
31
+ path: []
32
+ }];
33
+ for (let e of s) {
34
+ let i = (e.path ?? []).map((e) => String(typeof e == "object" ? e.key : e));
35
+ r.push(v("schema/invalid", "error", `${n}: ${e.message}`, {
36
+ file: t.file,
37
+ path: i.join(".") || "(root)"
38
+ }, "Correct the field to the v1 contract; unknown fields are not allowed."));
39
+ }
40
+ }
41
+ return a;
42
+ }, x = [
43
+ ["private key", /-----BEGIN [A-Z ]*PRIVATE KEY-----/],
44
+ ["Stripe key", /\b[sr]k_(live|test)_[A-Za-z0-9]{16,}/],
45
+ ["GitHub token", /\bgh[pousr]_[A-Za-z0-9]{30,}/],
46
+ ["AWS access key", /\bAKIA[0-9A-Z]{16}\b/],
47
+ ["Slack token", /\bxox[abprs]-[A-Za-z0-9-]{10,}/],
48
+ ["bearer token", /\bBearer\s+[A-Za-z0-9._~+/-]{20,}/]
49
+ ], S = (e, t, n, r) => {
50
+ if (typeof e == "string") for (let [i, a] of x) a.test(e) && r.push(v("security/secret", "security", `a ${i} is in the plan artefact`, {
51
+ file: t,
52
+ path: n
53
+ }, "Remove the secret, rotate it, and reference where it is kept instead."));
54
+ else if (Array.isArray(e)) e.forEach((e, i) => S(e, t, `${n}[${i}]`, r));
55
+ else if (typeof e == "object" && e) for (let [i, a] of Object.entries(e)) S(a, t, n === "" ? i : `${n}.${i}`, r);
56
+ }, C = (e) => {
57
+ let t = /* @__PURE__ */ new Set(), n = /* @__PURE__ */ new Map(), r = (r) => {
58
+ let i = [], a = /* @__PURE__ */ new Set(), o = (r) => {
59
+ i.push(r), a.add(r);
60
+ for (let s of [...e.get(r) ?? []].sort()) if (e.has(s)) {
61
+ if (a.has(s)) {
62
+ let e = i.slice(i.indexOf(s)), t = e.reduce((e, t) => t < e ? t : e), r = [...e.slice(e.indexOf(t)), ...e.slice(0, e.indexOf(t))];
63
+ n.set(r.join(">"), r);
64
+ } else t.has(s) || o(s);
65
+ }
66
+ i.pop(), a.delete(r), t.add(r);
67
+ };
68
+ o(r);
69
+ };
70
+ for (let n of [...e.keys()].sort()) t.has(n) || r(n);
71
+ return [...n.values()].sort((e, t) => e.join(">") < t.join(">") ? -1 : 1);
72
+ }, w = (m) => {
73
+ let { shape: h, clock: g } = m, _ = h.grammar, y = [];
74
+ for (let e of [
75
+ ...m.instances,
76
+ ...m.evidence ?? [],
77
+ ...m.findings ?? [],
78
+ ...m.closures ?? []
79
+ ]) S(e.content, e.file, "", y);
80
+ let x = b(m.instances, i(h), "plan instance", y).map(({ file: e, value: t }) => ({
81
+ file: e,
82
+ instance: t
83
+ })), w = (m.evidence ?? []).flatMap((e) => b(Array.isArray(e.content) ? e.content.map((t, n) => ({
84
+ file: `${e.file}#${n}`,
85
+ content: t
86
+ })) : [e], n(h), "evidence", y).map((e) => e.value)), T = (m.findings ?? []).flatMap((e) => b(Array.isArray(e.content) ? e.content.map((t, n) => ({
87
+ file: `${e.file}#${n}`,
88
+ content: t
89
+ })) : [e], r(h), "finding", y).map((e) => e.value)), E = /* @__PURE__ */ new Map(), D = /* @__PURE__ */ new Map(), O = /* @__PURE__ */ new Map();
90
+ for (let { file: n, instance: r } of x) {
91
+ let i = r.plan;
92
+ E.has(i.id) && y.push(v("identity/duplicate-plan", "error", `${i.id} is declared in two files`, {
93
+ file: n,
94
+ plan: i.id
95
+ }, "Keep one instance file per plan.", [E.get(i.id)])), E.set(i.id, n), t(i.id) !== i.number && y.push(v("identity/plan-number", "error", `${i.id} has number ${i.number}`, {
96
+ file: n,
97
+ plan: i.id,
98
+ path: "plan.number"
99
+ }, "Make the number match the plan id."));
100
+ for (let a of r.workItems) {
101
+ let r = {
102
+ file: n,
103
+ plan: i.id,
104
+ workItem: a.id
105
+ };
106
+ D.has(a.id) ? y.push(v("identity/duplicate-work-item", "error", `${a.id} is declared twice`, r, "Give the second item its own id; ids are never reused.", [D.get(a.id).file])) : D.set(a.id, {
107
+ file: n,
108
+ item: a
109
+ }), (a.ownerPlan !== i.id || t(a.id) !== i.number) && y.push(v("identity/owner-mismatch", "error", `${a.id} is owned by ${a.ownerPlan} but lives in ${i.id}`, r, "Move the item to its plan’s file or correct ownerPlan."));
110
+ let o = [
111
+ ...a.acceptance.map((e) => e.id),
112
+ ...a.evidenceRequirements.map((e) => e.id),
113
+ ...a.sourceRefs.map((e) => e.id)
114
+ ];
115
+ for (let t of o) e(_, t) !== a.id && y.push(v("identity/child-id", "error", `${t} does not belong to ${a.id}`, r, `Number it under ${a.id}.`));
116
+ for (let e of a.sourceRefs) O.has(e.id) && y.push(v("source/duplicate", "error", `source ${e.id} is disposed twice`, r, "Dispose each source block exactly once.", [O.get(e.id)])), O.set(e.id, a.id), e.disposition !== "owned-by" && e.targetId === void 0 && y.push(v("source/missing-target", "error", `source ${e.id} is ${e.disposition} without a target`, {
117
+ ...r,
118
+ path: `sourceRefs.${e.id}`
119
+ }, "Name the work item or decision it points to."));
120
+ }
121
+ if (m.sources !== void 0) {
122
+ let e = m.sources.get(i.source);
123
+ e == null ? y.push(v("source/missing", "error", `${i.id}'s source ${i.source} cannot be read`, {
124
+ file: n,
125
+ plan: i.id,
126
+ path: "plan.source"
127
+ }, "Restore the source or point the plan at it.")) : (e !== i.sourceRevision || e !== r.extraction.sourceRevision) && y.push(v("source/stale", "error", `${i.source} changed since ${i.id} was decomposed (${i.sourceRevision.slice(0, 19)}… → ${e.slice(0, 19)}…)`, {
128
+ file: n,
129
+ plan: i.id,
130
+ path: "plan.sourceRevision"
131
+ }, "Regenerate the plan’s decomposition from its source; closure waits until then."));
132
+ }
133
+ let a = r.workItems.reduce((e, t) => e + t.sourceRefs.length, 0);
134
+ (r.coverage.disposedSources !== a || r.coverage.unmatchedSources > 0 || r.coverage.duplicateSources > 0 || i.decompositionComplete && r.coverage.result !== "complete") && y.push(v("source/coverage", "error", `${i.id} claims ${r.coverage.disposedSources} disposed sources (${r.coverage.unmatchedSources} unmatched, ${r.coverage.duplicateSources} duplicate, ${r.coverage.result}) and holds ${a}`, {
135
+ file: n,
136
+ plan: i.id,
137
+ path: "coverage"
138
+ }, "Regenerate the plan’s decomposition from its source."));
139
+ }
140
+ let k = new Set(w.map((e) => e.id));
141
+ for (let { file: e, item: t } of D.values()) {
142
+ let n = {
143
+ file: e,
144
+ plan: t.ownerPlan,
145
+ workItem: t.id
146
+ };
147
+ for (let e of t.dependsOn) e === t.id ? y.push(v("graph/self-dependency", "error", `${t.id} depends on itself`, n, "Remove the dependency.")) : D.has(e) || y.push(v("graph/missing-dependency", "error", `${t.id} depends on ${e}, which does not exist`, n, "Point at an existing item or remove the dependency.", [e]));
148
+ if (m.decisions !== void 0) for (let e of t.links.adrs) m.decisions.has(e) || y.push(v("graph/missing-decision", "error", `${t.id} links ${e}, which the decision records do not hold`, n, "Add the decision record or correct the link.", [e]));
149
+ for (let e of t.links.relatedTo) D.has(e) || y.push(v("graph/missing-related", "error", `${t.id} relates to ${e}, which does not exist`, n, "Point at an existing item.", [e]));
150
+ t.supersededBy !== void 0 && !D.has(t.supersededBy) && y.push(v("graph/missing-successor", "error", `${t.id} is superseded by ${t.supersededBy}, which does not exist`, n, "Name the item that replaces it.", [t.supersededBy]));
151
+ let r = [
152
+ ...t.evidenceRefs,
153
+ ...t.acceptance.flatMap((e) => e.evidenceRefs),
154
+ ...t.evidenceRequirements.flatMap((e) => e.evidenceRefs),
155
+ ...t.statusHistory.flatMap((e) => e.evidenceRefs)
156
+ ];
157
+ for (let e of [...new Set(r)].sort()) k.has(e) || y.push(v("evidence/missing", "error", `${t.id} names evidence ${e}, which does not exist`, n, "Add the evidence with `voltro plan evidence add` or remove the reference.", [e]));
158
+ t.owners.length === 0 && y.push(v("quality/no-owner", "warning", `${t.id} has no owner`, n, "Name who owns the item."));
159
+ }
160
+ for (let e of C(new Map([...D.values()].map(({ item: e }) => [e.id, e.dependsOn])))) {
161
+ let t = D.get(e[0]);
162
+ y.push(v("graph/cycle", "error", `dependency cycle: ${[...e, e[0]].join(" → ")}`, {
163
+ file: t.file,
164
+ plan: t.item.ownerPlan,
165
+ workItem: e[0]
166
+ }, "Break the cycle: one of these items must not wait for the others.", e));
167
+ }
168
+ for (let e of T) for (let t of e.targetWorkItems) D.has(t) || y.push(v("graph/missing-finding-target", "error", `finding ${e.id} targets ${t}, which does not exist`, { workItem: t }, "Point the finding at an existing item.", [e.id]));
169
+ for (let { file: e, item: t } of D.values()) {
170
+ let n = {
171
+ file: e,
172
+ plan: t.ownerPlan,
173
+ workItem: t.id
174
+ };
175
+ for (let e of t.acceptance) e.status !== "pending" && e.evidenceRefs.length === 0 && y.push(v("acceptance/decided-without-evidence", "error", `${e.id} is ${e.status} without evidence`, {
176
+ ...n,
177
+ path: e.id
178
+ }, "Name the evidence the decision rests on."));
179
+ for (let e of t.evidenceRequirements) e.status === "satisfied" && e.evidenceRefs.length === 0 && y.push(v("acceptance/satisfied-without-evidence", "error", `${e.id} is satisfied without evidence`, {
180
+ ...n,
181
+ path: e.id
182
+ }, "Name the evidence that satisfies it.")), e.status === "waived" && e.waiver === void 0 && y.push(v("acceptance/waived-without-waiver", "error", `${e.id} is waived without a waiver`, {
183
+ ...n,
184
+ path: e.id
185
+ }, "Write the waiver: rationale, reviewer, expiry and compensating control."));
186
+ }
187
+ let A = l(w, T), j = /* @__PURE__ */ new Map(), M = /* @__PURE__ */ new Map(), N = /* @__PURE__ */ new Set(), P = (e) => {
188
+ let t = j.get(e);
189
+ if (t !== void 0) return t;
190
+ let n = D.get(e);
191
+ if (n === void 0 || N.has(e)) return;
192
+ N.add(e);
193
+ let r = u(n.item, A, P, g);
194
+ N.delete(e);
195
+ let i = c(n.item, r);
196
+ return j.set(e, i), M.set(e, r), i;
197
+ };
198
+ for (let { file: e, item: t } of [...D.values()].sort((e, t) => e.item.id < t.item.id ? -1 : 1)) {
199
+ let n = {
200
+ file: e,
201
+ plan: t.ownerPlan,
202
+ workItem: t.id
203
+ }, r = null;
204
+ t.statusHistory.forEach((e, i) => {
205
+ e.from === r ? e.from !== null && !s[e.from].includes(e.to) && y.push(v("state/forbidden-transition", "error", `${t.id} went ${e.from} → ${e.to}`, {
206
+ ...n,
207
+ path: `statusHistory[${i}]`
208
+ }, `From ${e.from} an item goes to ${s[e.from].join(", ")}.`)) : y.push(v("state/history-break", "error", `${t.id} history step ${i + 1} starts at ${e.from ?? "nothing"}, after ${r ?? "nothing"}`, {
209
+ ...n,
210
+ path: `statusHistory[${i}]`
211
+ }, "Record every transition in order.")), r = e.to;
212
+ }), r !== t.status && y.push(v("state/history-end", "error", `${t.id} is ${t.status} but its history ends at ${r ?? "nothing"}`, {
213
+ ...n,
214
+ path: "status"
215
+ }, "Record the transition that led to the current state.")), t.status === "blocked" && t.blocker === void 0 && y.push(v("state/blocked-without-blocker", "error", `${t.id} is blocked without a blocker`, {
216
+ ...n,
217
+ path: "blocker"
218
+ }, "Write down reason, owner, next action and recheck trigger.")), t.status !== "blocked" && t.blocker !== void 0 && y.push(v("state/blocker-not-blocked", "error", `${t.id} carries a blocker but is ${t.status}`, {
219
+ ...n,
220
+ path: "blocker"
221
+ }, "Remove the blocker or record the block.")), P(t.id);
222
+ }
223
+ let F = /* @__PURE__ */ new Map();
224
+ for (let { file: e, value: t } of b(m.closures ?? [], d(h), "closure record", y)) E.has(t.plan) ? F.has(t.plan) ? y.push(v("closure/duplicate", "error", `${t.plan} has two closure records`, {
225
+ file: e,
226
+ plan: t.plan
227
+ }, "Keep one closure record per plan.", [F.get(t.plan).file])) : F.set(t.plan, {
228
+ file: e,
229
+ record: t
230
+ }) : y.push(v("closure/unknown-plan", "error", `a closure record names ${t.plan}, which does not exist`, {
231
+ file: e,
232
+ plan: t.plan
233
+ }, "Remove it or correct the plan id."));
234
+ let I = /* @__PURE__ */ new Map(), L = /* @__PURE__ */ new Map();
235
+ for (let { instance: e } of x) {
236
+ let t = p(e, w, T, (e) => j.get(e), m.decisions);
237
+ I.set(e.plan.id, t);
238
+ let n = F.get(e.plan.id), r = f(n?.record, t);
239
+ if (L.set(e.plan.id, r), n !== void 0 && n.record.certificates.length > 0 && r.state !== "certified" && y.push(v("closure/stale-certificate", "warning", `${e.plan.id} changed after its certificate ${n.record.certificates.at(-1).id}: it is open again`, {
240
+ file: n.file,
241
+ plan: e.plan.id
242
+ }, "Verify what changed and run both closure passes again.")), r.state === "certified") for (let t of e.workItems) j.get(t.id) === "verified" && j.set(t.id, "closed");
243
+ e.plan.status === "complete" && r.state !== "certified" && y.push(v("state/manual-derived", "error", `${e.plan.id} says complete without a certificate on its current revision`, {
244
+ plan: e.plan.id,
245
+ path: "plan.status"
246
+ }, "Close it with both passes: voltro plan close <plan>."));
247
+ }
248
+ for (let { file: e, item: t } of [...D.values()].sort((e, t) => e.item.id < t.item.id ? -1 : 1)) {
249
+ let n = j.get(t.id);
250
+ o.has(t.status) && n !== t.status && (t.status !== "verified" || n !== "closed") && y.push(v("state/manual-derived", "error", `${t.id} says ${t.status}; the evidence says ${n}: ${(M.get(t.id) ?? []).slice(0, 3).join("; ") || "its plan holds no certificate on its current revision"}`, {
251
+ file: e,
252
+ plan: t.ownerPlan,
253
+ workItem: t.id,
254
+ path: "status"
255
+ }, "Add the missing evidence; verified and closed are derived, never set."));
256
+ }
257
+ return {
258
+ program: {
259
+ instances: x,
260
+ items: D,
261
+ evidence: w,
262
+ findings: T,
263
+ status: j,
264
+ gaps: M,
265
+ closures: F,
266
+ revisions: I,
267
+ closure: L
268
+ },
269
+ diagnostics: a(y)
270
+ };
271
+ };
272
+ //#endregion
273
+ export { C as dependencyCycles, w as lintProgram };
@@ -0,0 +1,16 @@
1
+ import { Schema as e } from "effect";
2
+ //#region src/manifest.ts
3
+ var t = "voltro-plan-manifest/v1", n = e.String.pipe(e.check(e.isMinLength(1)), e.check(e.makeFilter((e) => !e.startsWith("/") && !/^[A-Za-z]:/.test(e) && !e.split("/").includes("..") || "a path stays inside the project root"))), r = e.Struct({
4
+ schemaVersion: e.Literal(t),
5
+ prefix: e.String.pipe(e.check(e.isPattern(/^[A-Z][A-Z0-9]{1,9}$/))),
6
+ instanceVersion: e.String.pipe(e.check(e.isMinLength(1))),
7
+ repos: e.Array(e.String.pipe(e.check(e.isMinLength(1)))).pipe(e.check(e.isMinLength(1))),
8
+ instances: e.Array(n).pipe(e.check(e.isMinLength(1))),
9
+ evidence: e.optional(e.Array(n)),
10
+ evidenceDir: e.optional(n),
11
+ closureDir: e.optional(n),
12
+ adrDir: e.optional(n),
13
+ findings: e.optional(e.Array(n))
14
+ });
15
+ //#endregion
16
+ export { t as MANIFEST_SCHEMA_VERSION, r as Manifest };
package/dist/next.js ADDED
@@ -0,0 +1,64 @@
1
+ //#region src/next.ts
2
+ var e = {
3
+ P0: 0,
4
+ P1: 1,
5
+ P2: 2,
6
+ P3: 3
7
+ }, t = (t, n, r) => {
8
+ if (r) return {
9
+ outcome: "invalid",
10
+ reason: "lint reports blocking diagnostics; fix them first (`voltro plan lint`)"
11
+ };
12
+ let i = (e) => t.status.get(e), a = new Set([...t.items.values()].filter(({ item: e }) => [
13
+ "active",
14
+ "verified",
15
+ "closed"
16
+ ].includes(i(e.id) ?? "")).map(({ item: e }) => e.ownerPlan)), o = [...t.items.values()].filter(({ item: e }) => e.mandatory && e.supersededBy === void 0 && (n.plan === void 0 || e.ownerPlan === n.plan || e.ownerPlan.endsWith(`-PLAN-${n.plan}`)) && (n.owner === void 0 || e.owners.includes(n.owner)) && (n.repo === void 0 || e.repos.includes(n.repo)));
17
+ if (o.every(({ item: e }) => ["verified", "closed"].includes(i(e.id) ?? ""))) return { outcome: "complete" };
18
+ let s = (e) => e.dependsOn.every((e) => ["verified", "closed"].includes(i(e) ?? "")), c = (t) => [
19
+ i(t.id) === "active" ? 0 : 1,
20
+ e[t.priority],
21
+ t.kind === "security" ? 0 : 1,
22
+ +!a.has(t.ownerPlan),
23
+ +!s(t),
24
+ t.id
25
+ ], l = (e, t) => {
26
+ for (let n = 0; n < e.length; n++) if (e[n] !== t[n]) return e[n] < t[n];
27
+ return !1;
28
+ }, u = (e) => {
29
+ let t;
30
+ for (let n of o) {
31
+ if (!e.includes(i(n.item.id) ?? "")) continue;
32
+ let r = c(n.item);
33
+ (t === void 0 || l(r, t.rank)) && (t = {
34
+ entry: n,
35
+ rank: r
36
+ });
37
+ }
38
+ return t;
39
+ }, d = u(["active", "ready"]);
40
+ if (d !== void 0) {
41
+ let e = d.entry, t = i(e.item.id);
42
+ return {
43
+ outcome: "item",
44
+ workItem: e.item,
45
+ file: e.file,
46
+ why: [
47
+ t === "active" ? "it is already active" : "it is ready",
48
+ `priority ${e.item.priority}`,
49
+ ...e.item.kind === "security" ? ["security work comes first"] : [],
50
+ a.has(e.item.ownerPlan) ? `${e.item.ownerPlan} is already under way` : "no started plan has workable items",
51
+ d.rank[4] === 0 ? "its dependencies are verified" : "no dependency-ready item is left, so it waits on its dependencies"
52
+ ]
53
+ };
54
+ }
55
+ let f = u(["proposed"]), p = o.filter(({ item: e }) => i(e.id) === "proposed").length, m = o.filter(({ item: e }) => i(e.id) === "blocked").length;
56
+ return {
57
+ outcome: "blocked",
58
+ reason: `no mandatory item is active or ready: ${p} proposed await readiness${m > 0 ? `, ${m} blocked` : ""}`,
59
+ prepare: f?.entry.item ?? null,
60
+ file: f?.entry.file ?? null
61
+ };
62
+ };
63
+ //#endregion
64
+ export { t as nextItem };
package/dist/schema.js ADDED
@@ -0,0 +1,216 @@
1
+ import { Schema as e } from "effect";
2
+ //#region src/schema.ts
3
+ var t = e.String.pipe(e.check(e.isMinLength(1))), n = e.String.pipe(e.check(e.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/))), r = e.String.pipe(e.check(e.isPattern(/^sha256:[a-f0-9]{64}$/))), i = (t) => e.Array(t).pipe(e.check(e.makeFilter((e) => new Set(e).size === e.length || "items are listed twice"))), a = (t) => e.Array(t).pipe(e.check(e.isMinLength(1))), o = e.String.pipe(e.check(e.isPattern(/^[a-z0-9]+(?:[.:/-][a-z0-9]+)*$/))), s = [
4
+ "proposed",
5
+ "ready",
6
+ "active",
7
+ "blocked",
8
+ "verified",
9
+ "closed"
10
+ ], c = [
11
+ "proposed",
12
+ "decomposing",
13
+ "ready",
14
+ "active",
15
+ "verifying",
16
+ "closure-1",
17
+ "closure-2",
18
+ "complete"
19
+ ], l = [
20
+ "commit",
21
+ "typecheck",
22
+ "unit-test",
23
+ "integration-test",
24
+ "e2e",
25
+ "browser",
26
+ "schema",
27
+ "migration",
28
+ "roundtrip",
29
+ "documentation",
30
+ "agent-guide",
31
+ "template",
32
+ "security",
33
+ "privacy",
34
+ "accessibility",
35
+ "rights",
36
+ "operations",
37
+ "performance",
38
+ "research",
39
+ "review",
40
+ "artifact"
41
+ ], u = [
42
+ "P0",
43
+ "P1",
44
+ "P2",
45
+ "P3"
46
+ ], d = [
47
+ "architecture",
48
+ "feature",
49
+ "framework",
50
+ "integration",
51
+ "migration",
52
+ "operations",
53
+ "security",
54
+ "documentation",
55
+ "research",
56
+ "tooling"
57
+ ], f = (t) => e.String.pipe(e.check(e.isPattern(t))), p = (p) => {
58
+ let m = p.grammar, h = i(f(m.evidence)), g = e.Struct({
59
+ id: f(m.source),
60
+ uri: t,
61
+ locator: t,
62
+ excerpt: t,
63
+ lineHint: e.optional(e.Int.pipe(e.check(e.isGreaterThanOrEqualTo(1)))),
64
+ contentHash: r,
65
+ disposition: e.Literals([
66
+ "owned-by",
67
+ "referenced-by",
68
+ "decision",
69
+ "non-goal"
70
+ ]),
71
+ targetId: e.optional(e.Union([f(m.workItem), f(m.adr)]))
72
+ }), _ = e.Struct({
73
+ id: f(m.acceptance),
74
+ statement: t,
75
+ status: e.Literals([
76
+ "pending",
77
+ "passed",
78
+ "failed"
79
+ ]),
80
+ evidenceRefs: h
81
+ }), v = e.Struct({
82
+ rationale: t,
83
+ reviewer: t,
84
+ expiresAt: n,
85
+ compensatingControl: t
86
+ }), y = e.Struct({
87
+ id: f(m.requirement),
88
+ statement: t,
89
+ kinds: a(e.Literals(l)),
90
+ status: e.Literals([
91
+ "open",
92
+ "satisfied",
93
+ "waived"
94
+ ]),
95
+ evidenceRefs: h,
96
+ waiver: e.optional(v)
97
+ }), b = e.Struct({
98
+ capabilities: e.Array(t),
99
+ journeys: e.Array(t),
100
+ adrs: i(f(m.adr)),
101
+ risks: e.Array(t),
102
+ relatedTo: i(f(m.workItem))
103
+ }), x = e.Struct({
104
+ reason: t,
105
+ owner: t,
106
+ nextAction: t,
107
+ recheckTrigger: t,
108
+ since: n
109
+ }), S = e.Struct({
110
+ from: e.NullOr(e.Literals(s)),
111
+ to: e.Literals(s),
112
+ reason: t,
113
+ actor: t,
114
+ at: n,
115
+ evidenceRefs: h
116
+ }), C = e.Struct({
117
+ id: f(m.workItem),
118
+ title: t,
119
+ ownerPlan: f(m.plan),
120
+ slice: t,
121
+ kind: e.Literals(d),
122
+ priority: e.Literals(u),
123
+ mandatory: e.Boolean,
124
+ status: e.Literals(s),
125
+ owners: e.Array(t),
126
+ repos: a(e.Literals(p.repos)),
127
+ sourceRefs: a(g),
128
+ dependsOn: i(f(m.workItem)),
129
+ links: b,
130
+ acceptance: a(_),
131
+ evidenceRequirements: a(y),
132
+ evidenceRefs: h,
133
+ impactTags: a(o),
134
+ statusHistory: a(S),
135
+ blocker: e.optional(x),
136
+ supersededBy: e.optional(f(m.workItem)),
137
+ createdAt: n,
138
+ updatedAt: n
139
+ }), w = e.Struct({
140
+ id: f(m.plan),
141
+ number: e.String.pipe(e.check(e.isPattern(/^[0-9]{2}$/))),
142
+ title: t,
143
+ source: t,
144
+ sourceRevision: r,
145
+ status: e.Literals(c),
146
+ decompositionComplete: e.Boolean,
147
+ updatedAt: n
148
+ }), T = e.Struct({
149
+ rulesVersion: t,
150
+ sourceRevision: r,
151
+ blockKinds: e.Array(t),
152
+ extractedAt: n
153
+ }), E = e.Struct({
154
+ extractedSources: e.Int,
155
+ disposedSources: e.Int,
156
+ unmatchedSources: e.Int,
157
+ duplicateSources: e.Int,
158
+ sourceIdsHash: r,
159
+ result: e.Literals(["complete", "incomplete"])
160
+ });
161
+ return e.Struct({
162
+ schemaVersion: e.Literal(p.instanceVersion),
163
+ plan: w,
164
+ extraction: T,
165
+ workItems: e.Array(C),
166
+ coverage: E
167
+ });
168
+ }, m = (i) => e.Struct({
169
+ id: f(i.grammar.evidence),
170
+ kind: e.Literals(l),
171
+ title: t,
172
+ uri: t,
173
+ fingerprint: e.optional(r),
174
+ result: e.Literals([
175
+ "passed",
176
+ "failed",
177
+ "informational"
178
+ ]),
179
+ subjectRefs: e.Array(t),
180
+ command: e.optional(t),
181
+ environment: e.optional(t),
182
+ producedBy: t,
183
+ reviewedBy: e.optional(t),
184
+ observedAt: n,
185
+ idempotencyKey: e.optional(t)
186
+ }), h = (r) => e.Struct({
187
+ id: f(r.grammar.finding),
188
+ title: t,
189
+ severity: e.Literals([
190
+ "critical",
191
+ "high",
192
+ "medium",
193
+ "low"
194
+ ]),
195
+ status: e.Literals([
196
+ "open",
197
+ "resolved",
198
+ "accepted-risk"
199
+ ]),
200
+ source: e.Literals([
201
+ "contract-reconciliation",
202
+ "adversarial-audit",
203
+ "test",
204
+ "incident",
205
+ "research",
206
+ "review",
207
+ "production"
208
+ ]),
209
+ targetWorkItems: a(f(r.grammar.workItem)),
210
+ impactTags: e.Array(t),
211
+ evidenceRefs: e.optional(i(f(r.grammar.evidence))),
212
+ openedAt: n,
213
+ resolvedAt: e.optional(n)
214
+ });
215
+ //#endregion
216
+ export { l as EVIDENCE_KINDS, d as ITEM_KINDS, c as PLAN_STATUSES, u as PRIORITIES, s as WORK_ITEM_STATUSES, m as evidenceSchema, h as findingSchema, p as planInstanceSchema };
package/dist/state.js ADDED
@@ -0,0 +1,67 @@
1
+ //#region src/state.ts
2
+ var e = {
3
+ proposed: ["ready", "blocked"],
4
+ ready: [
5
+ "active",
6
+ "blocked",
7
+ "proposed"
8
+ ],
9
+ active: [
10
+ "blocked",
11
+ "verified",
12
+ "ready"
13
+ ],
14
+ blocked: [
15
+ "proposed",
16
+ "ready",
17
+ "active"
18
+ ],
19
+ verified: ["closed", "active"],
20
+ closed: ["active"]
21
+ }, t = /* @__PURE__ */ new Set(["verified", "closed"]), n = (e, t) => {
22
+ let n = /* @__PURE__ */ new Map();
23
+ for (let e of t) if (e.status === "open") for (let t of e.targetWorkItems) {
24
+ let r = n.get(t);
25
+ r === void 0 ? n.set(t, [e]) : r.push(e);
26
+ }
27
+ let r = /* @__PURE__ */ new Map();
28
+ for (let t of e) for (let e of new Set(t.subjectRefs)) {
29
+ let n = r.get(e);
30
+ n === void 0 ? r.set(e, [t]) : n.push(t);
31
+ }
32
+ return {
33
+ evidence: new Map(e.map((e) => [e.id, e])),
34
+ bySubject: r,
35
+ openFindings: n
36
+ };
37
+ }, r = (e, t, n, r) => e.result === "passed" && e.subjectRefs.includes(n) && (r === void 0 || r.includes(e.kind)) && Date.parse(e.observedAt) >= Date.parse(t.updatedAt), i = (e, t, n, i) => {
38
+ let a = [], o = t.bySubject.get(e.id) ?? [], s;
39
+ if (e.acceptance.length > 4 || e.acceptance.length + e.evidenceRequirements.length > 4 && e.acceptance.length + e.evidenceRequirements.filter((e) => e.status !== "waived").length > 4) {
40
+ let t = /* @__PURE__ */ new Set([...e.acceptance.map((e) => e.id), ...e.evidenceRequirements.filter((e) => e.status !== "waived").map((e) => e.id)]);
41
+ s = /* @__PURE__ */ new Map();
42
+ let n = Date.parse(e.updatedAt);
43
+ for (let e of o) if (e.result === "passed" && Date.parse(e.observedAt) >= n) for (let n of e.subjectRefs) {
44
+ if (!t.has(n)) continue;
45
+ let r = s.get(n);
46
+ r === void 0 ? s.set(n, /* @__PURE__ */ new Set([e.kind])) : r.add(e.kind);
47
+ }
48
+ }
49
+ let c = (t, n) => {
50
+ if (s === void 0) return o.some((i) => r(i, e, t, n));
51
+ let i = s.get(t);
52
+ return i !== void 0 && (n === void 0 || n.some((e) => i.has(e)));
53
+ }, l = (n, i) => n.some((n) => {
54
+ let a = t.evidence.get(n);
55
+ return a !== void 0 && r(a, e, e.id, i);
56
+ });
57
+ for (let t of e.acceptance) !c(t.id) && !(t.status === "passed" && l(t.evidenceRefs)) && a.push(`acceptance ${t.id} has no passing evidence`);
58
+ for (let t of e.evidenceRequirements) t.status === "waived" ? (t.waiver === void 0 || Date.parse(t.waiver.expiresAt) <= i.now().getTime()) && a.push(`the waiver of ${t.id} has expired`) : !c(t.id, t.kinds) && !l(t.evidenceRefs, t.kinds) && a.push(`requirement ${t.id} has no passing ${t.kinds.join("/")} evidence`);
59
+ for (let n of t.openFindings.get(e.id) ?? []) a.push(`finding ${n.id} is open`);
60
+ for (let t of e.dependsOn) {
61
+ let e = n(t);
62
+ e !== "verified" && e !== "closed" && a.push(`dependency ${t} is ${e ?? "missing"}`);
63
+ }
64
+ return a;
65
+ }, a = (e, n) => n.length === 0 ? "verified" : t.has(e.status) ? "active" : e.status;
66
+ //#endregion
67
+ export { t as DERIVED, e as TRANSITIONS, a as derivedStatus, n as indexEvidence, i as verificationGaps };
package/dist/status.js ADDED
@@ -0,0 +1,35 @@
1
+ import { WORK_ITEM_STATUSES as e } from "./schema.js";
2
+ //#region src/status.ts
3
+ var t = () => Object.fromEntries(e.map((e) => [e, 0])), n = (e) => {
4
+ let n = t();
5
+ return {
6
+ plans: e.instances.map(({ file: r, instance: i }) => {
7
+ let a = t(), o = t(), s = [];
8
+ for (let t of i.workItems) {
9
+ let r = e.status.get(t.id) ?? t.status;
10
+ (t.mandatory ? a : o)[r]++, t.mandatory && n[r]++, r === "blocked" && t.blocker !== void 0 && s.push({
11
+ workItem: t.id,
12
+ reason: t.blocker.reason,
13
+ owner: t.blocker.owner,
14
+ nextAction: t.blocker.nextAction
15
+ });
16
+ }
17
+ let c = i.workItems.filter((e) => e.mandatory).length, l = a.verified + a.closed, u = e.closure.get(i.plan.id), d = i.plan.decompositionComplete ? u?.state === "certified" ? "complete" : c > 0 && l === c ? u?.state === "closure-1" ? "closure-1" : "verifying" : a.active + a.blocked + l > 0 ? "active" : a.ready > 0 ? "ready" : "decomposing" : "decomposing";
18
+ return {
19
+ plan: i.plan.id,
20
+ title: i.plan.title,
21
+ file: r,
22
+ recorded: i.plan.status,
23
+ derived: d,
24
+ certificate: u?.state === "certified" ? u.certificate.id : null,
25
+ mandatory: a,
26
+ optional: o,
27
+ blocked: s.sort((e, t) => e.workItem < t.workItem ? -1 : 1)
28
+ };
29
+ }).sort((e, t) => e.plan < t.plan ? -1 : 1),
30
+ mandatory: n,
31
+ items: e.items.size
32
+ };
33
+ };
34
+ //#endregion
35
+ export { n as programStatus };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plan",
3
- "version": "0.85.0",
3
+ "version": "0.87.0",
4
4
  "description": "The executable plan contract: ids, the v1 schema, lint, derived state, status and the next item — pure and browser-safe; the voltro plan CLI feeds it.",
5
5
  "keywords": [
6
6
  "voltro",