forma-arch 0.10.1 → 0.11.1

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/README.md CHANGED
@@ -77,6 +77,53 @@ Column roles are detected by header name (`capability`/`feature`/`description`
77
77
 
78
78
  The overlay is the authority: every field it sets wins over anything derived, and `check` stops re-deriving that field. You do not have to hand-edit that JSON — `forma gen --status-apply <file>` merges `{"nodes":{"<id>":{…}}}` into it, validating every patch **before** it touches disk, so a rejected fill leaves the committed overlay untouched. It is the state counterpart of `--enrich-apply`, for the fields no document can supply. To describe one box by hand without a table, put the sentence in `descriptions`, keyed `"<containerId>/<node-name-without-extension>"` — `"core/alpha"` for the leaf `alpha.js` in container `core`, `"core/core"` for the container itself, since a container is its own container. It outranks every other source.
79
79
 
80
+ **Future architecture can be a governed timeline.** Add an optional `timeline` to the curated
81
+ `c4-topology.json` when one binary target is not enough. The generated `nodes` and `edges` remain
82
+ the code-verified `AS-IS`; each checkpoint applies a compact typed patch over every checkpoint
83
+ before it. Forma never stores a second complete graph, never reads a board count as architecture,
84
+ and validates every cumulative state before replacing the last model.
85
+
86
+ ```json
87
+ {
88
+ "timeline": {
89
+ "source": "docs/architecture/FUTURE.md",
90
+ "checkpoints": [{
91
+ "id": "g1", "label": "G1 · UAT", "badge": "9 board · 1 P0",
92
+ "patch": {
93
+ "nodes": {
94
+ "add": [{ "node": {
95
+ "id": "mcp", "level": "container", "parent": "system",
96
+ "kind": "container", "name": "MCP channel", "status": "planned"
97
+ }, "change": "Add the MCP surface." }],
98
+ "update": [{ "id": "engine", "set": {
99
+ "current": "Serves HTTP and MCP through one core."
100
+ }, "change": "Extend the existing engine." }]
101
+ },
102
+ "edges": {
103
+ "add": [{ "edge": {
104
+ "from": "mcp", "to": "engine", "label": "delegates",
105
+ "estatus": "to-build"
106
+ }, "change": "Connect the new surface to the engine." }]
107
+ }
108
+ }
109
+ }]
110
+ }
111
+ }
112
+ ```
113
+
114
+ Node patches support `add`, non-structural `update`, and child-first `remove`; edge patches support
115
+ `add`, exact-match `rewire`, and `remove`. Every operation carries governed change prose; a live
116
+ node changed locally exposes it as “Change from previous”, while the graph accents changed nodes
117
+ and relations without keeping a second change log or drawing ghosts for removals. IDs, parents,
118
+ levels and kinds cannot be edited in place, ambiguous edge selectors fail, and a node cannot
119
+ disappear while children or incident relations survive. The source path must exist. `AS-IS` is
120
+ implicit and reserved; the last checkpoint is the target, so `target` is forbidden inside timeline
121
+ patches. With a timeline the viewer replaces CURRENT/TARGET with the checkpoints, supports
122
+ `?checkpoint=g1`, accents only the local patch, and keeps drill, layout, re-verification and export
123
+ working on the cumulative graph. A future checkpoint with no local patch says “no architecture
124
+ changes”; its display-only badge is not interpreted. Without a timeline, the legacy controls and
125
+ model are unchanged.
126
+
80
127
  **Curated state, verified against reality.** `forma verify` asks your `gh` CLI for the state of every issue the model references (`--gh-repo owner/repo`, or `meta.ghRepo` in the topology), marks the nodes whose issues are closed as done, and prefixes their `current` with dated evidence. It touches state, never structure, and re-running it never stacks the evidence. It is opt-in and separate on purpose: `gen` and `check` never open a socket. In the served viewer, **RE-VERIFY** re-reads the model without losing your level, layout or mode.
81
128
 
82
129
  **One source of truth.** `forma doc --attach docs/architecture/arc42.md` injects the generated diagrams/tables between `<!-- forma:begin -->` / `<!-- forma:end -->` markers in your existing doc; your prose lives outside them, and `forma check` fails if that block drifts. Attached files are recorded in `source.attachedDocs`, so the gate governs **every** doc you attach — not just the model's `docPath`; deleting the markers (or the file) from a registered doc fails the check rather than quietly un-governing it. That registry lives in `c4-model.json`, so **commit the model** — a lost model takes the registry with it. Where a repo lacks docs, `forma gen --enrich` can fill the remaining box holes with an LLM — opt-in, cached, never on the deterministic gate:
@@ -121,7 +168,9 @@ contract. The Claude skill in `adapters/` is a thin wrapper, not the product.
121
168
 
122
169
  The viewer is a live C4 map, not a static picture:
123
170
 
124
- - **Click any box** to read its explanation — what it does, current state, target, verification source — at *every* level, from context down to a leaf.
171
+ - **Click any box** to read its explanation — what it does, current state/target on legacy models,
172
+ or the state and local change at the selected checkpoint — at *every* level, from context down to
173
+ a leaf.
125
174
  - **Double-click a box** (or its `[+] DRILL`) to descend into it; **BACK**, the breadcrumb, or `ESC` climb back out.
126
175
  - **Drag boxes** to lay out the view your way; **RESET LAYOUT** restores the arrangement (your curated hints if the topology has them, the automatic one otherwise). To keep a layout, drag it, pick **Export layout JSON**, and paste the result under `"layout"` in the topology — `gen` carries it into `meta.layout` and the viewer pins those boxes, auto-arranging everything else clear of them.
127
176
  - **Arrow labels** are painted on the diagram while the level stays readable (≤14 arrows) and turn off above that; **LABELS** forces them on or off, and hovering an arrow always reveals its label.
package/lib/check.mjs CHANGED
@@ -10,7 +10,7 @@ import { containerOf } from './cluster.mjs'
10
10
  import { renderBlock, extractBetween, norm } from './render.mjs'
11
11
  import { descInputHash } from './enrich.mjs'
12
12
  import { loadDocRows, indexByNode, describingRows, statusFor } from './docmap.mjs'
13
- import { validateModel } from './validate.mjs'
13
+ import { materializeTimeline, validateModel } from './validate.mjs'
14
14
 
15
15
  const HERE = dirname(fileURLToPath(import.meta.url))
16
16
  const arg = (f, d) => { const i = process.argv.indexOf(f); return i > -1 ? process.argv[i + 1] : d }
@@ -26,6 +26,13 @@ if (!existsSync(TOPO)) { console.error('[check-c4] FAIL: topology missing: ' + T
26
26
  const model = JSON.parse(readFileSync(MODEL, 'utf-8'))
27
27
  const topo = JSON.parse(readFileSync(TOPO, 'utf-8'))
28
28
  for (const err of validateModel(model)) errs.push('SCHEMA: ' + err)
29
+ // The topology is the curated architecture authority. The generated model carries the same compact
30
+ // patch programme for the browser; editing either side alone is drift even when both JSON values
31
+ // remain individually valid.
32
+ if (JSON.stringify(model.timeline || null) !== JSON.stringify(topo.timeline || null)) {
33
+ fail('TIMELINE DRIFT: model.timeline does not match c4-topology.json — regenerate c4-model.json.')
34
+ }
35
+ for (const err of materializeTimeline(model, { sourceExists: (rel) => existsSync(rp(rel)) }).errors) fail('TIMELINE: ' + err)
29
36
  const byId = new Map((model.nodes || []).map((n) => [n.id, n]))
30
37
 
31
38
  // 1) schemaVersion present + basic shape
package/lib/docmap.mjs CHANGED
@@ -101,6 +101,7 @@ export function loadDocRows(repo, docSources, inventoryOnly = false) {
101
101
  if (!existsSync(abs)) continue
102
102
  let text
103
103
  try { text = readFileSync(abs, 'utf-8') } catch { continue }
104
+ const before = out.length
104
105
  for (const t of tables(text)) {
105
106
  const iD = roleIndex(t.header, 'describe', src.describe)
106
107
  const iM = roleIndex(t.header, 'ref', src.ref)
@@ -115,6 +116,15 @@ export function loadDocRows(repo, docSources, inventoryOnly = false) {
115
116
  done: iS < 0 ? null : DONE.test(String(r[iS]).replace(/[*_`]+/g, '').trim()), from: rel })
116
117
  }
117
118
  }
119
+ // A status column that never once says "not done" is an INVENTORY, not a measure. `done /
120
+ // rows.length` is then pinned to 1 by arithmetic, for every node and every subset of rows:
121
+ // haben's feature matrix is 39 rows, 39 DONE, and all 27 nodes it reached came out at exactly
122
+ // 100 — a constant a stakeholder reads as a measurement. The document is still saying
123
+ // something (it declares those capabilities finished), so the VERDICT survives; the percentage
124
+ // does not. Per source, because that is the unit that either discriminates or does not.
125
+ const mine = out.slice(before)
126
+ const measures = mine.some((r) => r.done === false)
127
+ for (const r of mine) r.measures = measures
118
128
  }
119
129
  return out
120
130
  }
@@ -147,7 +157,11 @@ export function indexByNode(rows, nodes) {
147
157
  const paths = subtree(n.id)
148
158
  if (!paths.length) continue
149
159
  const hit = rows.filter((r) => r.refs.some((ref) => paths.some((p) => touches(ref, p))))
150
- if (hit.length) idx.set(n.id, { rows: hit, cover: coverOf(n, byId, kids, hit, paths) })
160
+ // `located`: does this node have evidence of its OWN? A curated grouping box has none — every
161
+ // path in its subtree belongs to a child — so no row ever names IT, only things inside it.
162
+ // Reporting what is underneath is the viewer's job (rollStatus); claiming it as the box's own
163
+ // verdict is not. See statusFor.
164
+ if (hit.length) idx.set(n.id, { rows: hit, cover: coverOf(n, byId, kids, hit, paths), located: (n.evidence || []).length > 0 })
151
165
  }
152
166
  return idx
153
167
  }
@@ -176,19 +190,44 @@ export const describingRows = (idx, id) => {
176
190
  const e = idx.get(id)
177
191
  return e && e.rows.length <= MAX_ROWS ? e.rows : null
178
192
  }
193
+
194
+ // The rows that RULE on a node: every row that names it, cap or no cap. MAX_ROWS is a limit on how
195
+ // much prose a box can carry honestly; counting how many named capabilities are finished is not
196
+ // prose, and inheriting the cap made the verdict scale backwards — the more rows named a module,
197
+ // the less it was judged. `internal_budget`, named by four DONE rows, came out unassessed next to
198
+ // modules nobody documented at all (#43).
199
+ export const rulingRows = (idx, id) => {
200
+ const e = idx.get(id)
201
+ return e && e.rows.length ? e.rows : null
202
+ }
179
203
  export const coverageFor = (idx, id) => (idx.get(id) || {}).cover || null
180
204
 
181
205
  // Programme state a document states outright: how many of the capabilities living in this node are
182
206
  // finished. Derived, never curated — the c4-status.json overlay still overrides it (gen.mjs §WP-A1),
183
207
  // and `check` re-derives it from the same document rather than trusting the committed model.
208
+ //
209
+ // A DECLARATION, and only sometimes a measurement. The verdict is what the document says; the
210
+ // percentage is only emitted when the document's status column can say no (see `measures` in
211
+ // loadDocRows). A number nobody could have varied is not a measurement, and on a board it is read
212
+ // as one — which is worse than no number at all.
184
213
  export function statusFor(idx, id) {
185
- const rows = describingRows(idx, id)
214
+ const rows = rulingRows(idx, id)
186
215
  if (!rows || rows.some((r) => r.done == null)) return null // no status column: describe only
187
- const done = rows.filter((r) => r.done).length
216
+ // A box the document cannot name did not get ruled on: the rows reached it only through its
217
+ // children. The demo's `fisco` and `accesso` are curated groupings with no evidence of their own,
218
+ // and both came out done/100 with coverage {named:0} — a verdict borrowed from below and printed
219
+ // as the box's own. Reporting what is underneath is the viewer's job (rollStatus), not gen's.
220
+ if (!(idx.get(id) || {}).located) return null
188
221
  const c = coverageFor(idx, id)
222
+ const done = rows.filter((r) => r.done).length
223
+ // Past the cap the document TOUCHES the node rather than describing it (see MAX_ROWS), and the
224
+ // two halves of the answer part company: "of the rows that name this, N are done" is still
225
+ // something the document says, so the verdict stands — but a percentage over a reach nobody can
226
+ // state is the invented number this file exists to refuse. Verdict yes, percentage no.
227
+ const described = rows.length <= MAX_ROWS
189
228
  return {
190
229
  status2: done === rows.length ? 'done' : done === 0 ? 'planned' : 'in-progress',
191
- completion: Math.round((done / rows.length) * 100),
230
+ ...(described && rows.every((r) => r.measures) ? { completion: Math.round((done / rows.length) * 100) } : {}),
192
231
  // The citation says what it is: a repo document declaring itself finished, not a verification.
193
232
  source: `${rows[0].from} (${done}/${rows.length} declared done)`,
194
233
  ...(c ? { coverage: c } : {}),
package/lib/gen.mjs CHANGED
@@ -11,14 +11,14 @@ import { isGo, goEdges } from './lang.mjs'
11
11
  import { makeDescribeCtx, resolveDescription } from './describe.mjs'
12
12
  import { loadDocRows, indexByNode, describingRows, statusFor } from './docmap.mjs'
13
13
  import { loadCache, mergeCache, enrich, agentPlan, applyFills } from './enrich.mjs'
14
- import { validateModel } from './validate.mjs'
14
+ import { materializeTimeline, validateModel } from './validate.mjs'
15
15
 
16
16
  const HERE = dirname(fileURLToPath(import.meta.url))
17
17
  const arg = (f, d) => { const i = process.argv.indexOf(f); return i > -1 ? process.argv[i + 1] : d }
18
18
  const REPO = arg('--repo', process.cwd())
19
19
  const TOPO = arg('--topology', join(REPO, 'docs/architecture/c4-topology.json'))
20
20
  const OUT = arg('--out', join(REPO, 'docs/architecture/c4-model.json'))
21
- const SCHEMA_VERSION = '1.5.0' // +descSource:"docmap" (additive enum value → MINOR)
21
+ const SCHEMA_VERSION = '1.6.0' // +optional cumulative architecture timeline (additive)
22
22
  const CLUSTER = !process.argv.includes('--no-cluster') // §2: auto-cluster flat containers; --no-cluster to disable
23
23
  const ENRICH = process.argv.includes('--enrich') // §7: opt-in LLM prose for description holes
24
24
  const ENRICHER = arg('--enricher', null)
@@ -168,7 +168,10 @@ for (const n of nodes) {
168
168
  // `coverage` rides in `verify` (additionalProperties: true there, so no schema churn) and says how
169
169
  // much of the node the citation actually reaches — the difference between "3 of 3 rows are done"
170
170
  // and "this module is done".
171
- if (ds) { n.status2 = ds.status2; if (n.completion == null) n.completion = ds.completion; if (!n.verify) n.verify = { source: ds.source, derived: true, ...(ds.coverage ? { coverage: ds.coverage } : {}) } }
171
+ // `ds.completion` is ABSENT when the document declares rather than measures (docmap §statusFor).
172
+ // Assigning it anyway would put an own `completion: undefined` on the node — invisible in the
173
+ // written JSON, but validateModel walks own properties and would reject it against `integer`.
174
+ if (ds) { n.status2 = ds.status2; if (n.completion == null && ds.completion != null) n.completion = ds.completion; if (!n.verify) n.verify = { source: ds.source, derived: true, ...(ds.coverage ? { coverage: ds.coverage } : {}) } }
172
175
  // Code can prove a file EXISTS; it cannot prove the work behind it is finished. Marking every
173
176
  // undecorated node done/100 turned a virgin repo into a board reading "10/10 complete" — the
174
177
  // exact false green this tool exists to kill. No overlay (§WP-A1), no verdict: `unknown`, and
@@ -380,6 +383,7 @@ const model = {
380
383
  schemaVersion: SCHEMA_VERSION,
381
384
  generatedAt: new Date().toISOString(),
382
385
  source: { repo: topo.meta.repo, commit, branch, docPath: topo.docPath, generator: 'gen-c4-model@' + SCHEMA_VERSION, ...(attachedDocs.length ? { attachedDocs } : {}), ...(statusPath ? { statusPath } : {}) },
386
+ ...(topo.timeline ? { timeline: topo.timeline } : {}),
383
387
  // topo.layout (optional curated coordinates, keyed by parent id) rides along verbatim: the
384
388
  // viewer pins those nodes and auto-arranges the rest. `meta` is the model's free-form area.
385
389
  meta: { ...topo.meta, ...(topo.layout ? { layout: topo.layout } : {}), verifyMethod: 'code+topology (gh re-verify optional)' },
@@ -402,11 +406,14 @@ if (process.argv.includes('--from-docs')) {
402
406
  console.log(`[gen-c4] --from-docs: ${planned.length} planned milestone(s), ${accepted}/${adrs} ADR accepted`)
403
407
  } catch (e) { model.meta.verifyError = 'from-docs: ' + String((e && e.message) || e) }
404
408
  }
405
- writeFileSync(OUT, JSON.stringify(model, null, 2) + '\n')
406
- // topo.nodes are copied verbatim into the model (pass 1), so a bad curated kind used to leak in
407
- // and only fail later this catches it immediately and keeps invalid artifacts from being written.
409
+ // topo.nodes and timeline patches are copied into the model, so validate both the static schema and
410
+ // EVERY cumulative checkpoint before touching OUT. A rejected projection must not corrupt the last
411
+ // committed model any more than a rejected status overlay does.
408
412
  const schemaErrors = validateModel(model)
409
413
  if (schemaErrors.length) fail('model does not validate against lib/schema/c4-model.schema.json:\n - ' + schemaErrors.join('\n - '))
414
+ const timelineErrors = materializeTimeline(model, { sourceExists: (rel) => existsSync(rp(rel)) }).errors
415
+ if (timelineErrors.length) fail('timeline is not a valid cumulative architecture projection:\n - ' + timelineErrors.join('\n - '))
416
+ writeFileSync(OUT, JSON.stringify(model, null, 2) + '\n')
410
417
  const counts = { total: nodes.length, leaves: nodes.filter((n) => n.kind === 'leaf').length, planned: nodes.filter((n) => n.status === 'planned').length }
411
418
  console.log(`[gen-c4] wrote ${OUT}`)
412
419
  console.log(`[gen-c4] nodes=${counts.total} leaves=${counts.leaves} planned=${counts.planned} edges=${model.edges.length} commit=${commit.slice(0,8)}`)
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://github.com/LucaDominici/forma/blob/main/lib/schema/c4-model.schema.json",
4
4
  "title": "C4 model \u2014 stack-agnostic architecture graph",
5
- "description": "Reusable contract for the C4 live dashboard. A generator conforms iff its output validates; the viewer renders anything that validates. Stack-specific facts live only in tech/meta.",
5
+ "description": "Reusable structural contract for the C4 live dashboard. A generator conforms iff its output validates and every optional timeline checkpoint materializes semantically; the viewer renders generator-conformant models. Stack-specific facts live only in tech/meta.",
6
6
  "type": "object",
7
7
  "required": [
8
8
  "schemaVersion",
@@ -61,6 +61,319 @@
61
61
  }
62
62
  }
63
63
  },
64
+ "timeline": {
65
+ "type": "object",
66
+ "description": "Optional governed future architecture. The generated nodes/edges remain the AS-IS baseline; ordered checkpoint patches are applied cumulatively.",
67
+ "required": [
68
+ "source",
69
+ "checkpoints"
70
+ ],
71
+ "additionalProperties": false,
72
+ "properties": {
73
+ "source": {
74
+ "type": "string",
75
+ "description": "Repo-relative architecture document governing the future projection."
76
+ },
77
+ "checkpoints": {
78
+ "type": "array",
79
+ "minItems": 1,
80
+ "items": {
81
+ "type": "object",
82
+ "required": [
83
+ "id",
84
+ "label"
85
+ ],
86
+ "additionalProperties": false,
87
+ "properties": {
88
+ "id": {
89
+ "type": "string",
90
+ "pattern": "^[a-z0-9][a-z0-9._-]*$"
91
+ },
92
+ "label": {
93
+ "type": "string"
94
+ },
95
+ "badge": {
96
+ "type": "string",
97
+ "description": "Display-only checkpoint annotation. Never interpreted as architecture operations."
98
+ },
99
+ "patch": {
100
+ "type": "object",
101
+ "additionalProperties": false,
102
+ "properties": {
103
+ "nodes": {
104
+ "type": "object",
105
+ "additionalProperties": false,
106
+ "properties": {
107
+ "add": {
108
+ "type": "array",
109
+ "items": {
110
+ "type": "object",
111
+ "required": [
112
+ "node",
113
+ "change"
114
+ ],
115
+ "additionalProperties": false,
116
+ "properties": {
117
+ "node": {
118
+ "$ref": "#/properties/nodes/items"
119
+ },
120
+ "change": {
121
+ "type": "string"
122
+ }
123
+ }
124
+ }
125
+ },
126
+ "update": {
127
+ "type": "array",
128
+ "items": {
129
+ "type": "object",
130
+ "required": [
131
+ "id",
132
+ "set",
133
+ "change"
134
+ ],
135
+ "additionalProperties": false,
136
+ "properties": {
137
+ "id": {
138
+ "type": "string"
139
+ },
140
+ "set": {
141
+ "type": "object",
142
+ "minProperties": 1,
143
+ "additionalProperties": false,
144
+ "properties": {
145
+ "name": {
146
+ "$ref": "#/properties/nodes/items/properties/name"
147
+ },
148
+ "drill": {
149
+ "$ref": "#/properties/nodes/items/properties/drill"
150
+ },
151
+ "tech": {
152
+ "$ref": "#/properties/nodes/items/properties/tech"
153
+ },
154
+ "description": {
155
+ "$ref": "#/properties/nodes/items/properties/description"
156
+ },
157
+ "status": {
158
+ "$ref": "#/properties/nodes/items/properties/status"
159
+ },
160
+ "link": {
161
+ "$ref": "#/properties/nodes/items/properties/link"
162
+ },
163
+ "evidence": {
164
+ "$ref": "#/properties/nodes/items/properties/evidence"
165
+ },
166
+ "meta": {
167
+ "$ref": "#/properties/nodes/items/properties/meta"
168
+ },
169
+ "category": {
170
+ "$ref": "#/properties/nodes/items/properties/category"
171
+ },
172
+ "status2": {
173
+ "$ref": "#/properties/nodes/items/properties/status2"
174
+ },
175
+ "completion": {
176
+ "$ref": "#/properties/nodes/items/properties/completion"
177
+ },
178
+ "statusWord": {
179
+ "$ref": "#/properties/nodes/items/properties/statusWord"
180
+ },
181
+ "current": {
182
+ "$ref": "#/properties/nodes/items/properties/current"
183
+ },
184
+ "issues": {
185
+ "$ref": "#/properties/nodes/items/properties/issues"
186
+ },
187
+ "verify": {
188
+ "$ref": "#/properties/nodes/items/properties/verify"
189
+ },
190
+ "func": {
191
+ "$ref": "#/properties/nodes/items/properties/func"
192
+ },
193
+ "descSource": {
194
+ "$ref": "#/properties/nodes/items/properties/descSource"
195
+ },
196
+ "descInputHash": {
197
+ "$ref": "#/properties/nodes/items/properties/descInputHash"
198
+ }
199
+ }
200
+ },
201
+ "change": {
202
+ "type": "string"
203
+ }
204
+ }
205
+ }
206
+ },
207
+ "remove": {
208
+ "type": "array",
209
+ "items": {
210
+ "type": "object",
211
+ "required": [
212
+ "id",
213
+ "change"
214
+ ],
215
+ "additionalProperties": false,
216
+ "properties": {
217
+ "id": {
218
+ "type": "string"
219
+ },
220
+ "change": {
221
+ "type": "string"
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ },
228
+ "edges": {
229
+ "type": "object",
230
+ "additionalProperties": false,
231
+ "properties": {
232
+ "add": {
233
+ "type": "array",
234
+ "items": {
235
+ "type": "object",
236
+ "required": [
237
+ "edge",
238
+ "change"
239
+ ],
240
+ "additionalProperties": false,
241
+ "properties": {
242
+ "edge": {
243
+ "$ref": "#/properties/edges/items"
244
+ },
245
+ "change": {
246
+ "type": "string"
247
+ }
248
+ }
249
+ }
250
+ },
251
+ "rewire": {
252
+ "type": "array",
253
+ "items": {
254
+ "type": "object",
255
+ "required": [
256
+ "match",
257
+ "set",
258
+ "change"
259
+ ],
260
+ "additionalProperties": false,
261
+ "properties": {
262
+ "match": {
263
+ "type": "object",
264
+ "required": [
265
+ "from",
266
+ "to"
267
+ ],
268
+ "additionalProperties": false,
269
+ "properties": {
270
+ "from": {
271
+ "type": "string"
272
+ },
273
+ "to": {
274
+ "type": "string"
275
+ },
276
+ "label": {
277
+ "type": "string"
278
+ },
279
+ "kind": {
280
+ "type": "string"
281
+ }
282
+ }
283
+ },
284
+ "set": {
285
+ "type": "object",
286
+ "minProperties": 1,
287
+ "additionalProperties": false,
288
+ "anyOf": [
289
+ {
290
+ "required": [
291
+ "from"
292
+ ]
293
+ },
294
+ {
295
+ "required": [
296
+ "to"
297
+ ]
298
+ }
299
+ ],
300
+ "properties": {
301
+ "from": {
302
+ "$ref": "#/properties/edges/items/properties/from"
303
+ },
304
+ "to": {
305
+ "$ref": "#/properties/edges/items/properties/to"
306
+ },
307
+ "label": {
308
+ "$ref": "#/properties/edges/items/properties/label"
309
+ },
310
+ "kind": {
311
+ "$ref": "#/properties/edges/items/properties/kind"
312
+ },
313
+ "status": {
314
+ "$ref": "#/properties/edges/items/properties/status"
315
+ },
316
+ "evidence": {
317
+ "$ref": "#/properties/edges/items/properties/evidence"
318
+ },
319
+ "estatus": {
320
+ "$ref": "#/properties/edges/items/properties/estatus"
321
+ }
322
+ }
323
+ },
324
+ "change": {
325
+ "type": "string"
326
+ }
327
+ }
328
+ }
329
+ },
330
+ "remove": {
331
+ "type": "array",
332
+ "items": {
333
+ "type": "object",
334
+ "required": [
335
+ "match",
336
+ "change"
337
+ ],
338
+ "additionalProperties": false,
339
+ "properties": {
340
+ "match": {
341
+ "type": "object",
342
+ "required": [
343
+ "from",
344
+ "to"
345
+ ],
346
+ "additionalProperties": false,
347
+ "properties": {
348
+ "from": {
349
+ "type": "string"
350
+ },
351
+ "to": {
352
+ "type": "string"
353
+ },
354
+ "label": {
355
+ "type": "string"
356
+ },
357
+ "kind": {
358
+ "type": "string"
359
+ }
360
+ }
361
+ },
362
+ "change": {
363
+ "type": "string"
364
+ }
365
+ }
366
+ }
367
+ }
368
+ }
369
+ }
370
+ }
371
+ }
372
+ }
373
+ }
374
+ }
375
+ }
376
+ },
64
377
  "meta": {
65
378
  "type": "object",
66
379
  "description": "Repo-level escape hatch (archetype, stack, overlays, version). Never required by the viewer.",