@ecoma-io/archkeep 0.16.1 → 0.18.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/README.md +1 -1
- package/cli.mjs +258 -20
- package/package.json +2 -2
- package/src/architecture-intent/judge.mjs +19 -6
- package/src/commands/adr.mjs +45 -4
- package/src/commands/change-intent.mjs +55 -8
- package/src/commands/change.mjs +332 -11
- package/src/commands/debt.mjs +26 -5
- package/src/commands/decisions.mjs +291 -0
- package/src/commands/delta-classify.mjs +257 -0
- package/src/commands/delta.mjs +269 -8
- package/src/commands/evolution.mjs +758 -5
- package/src/commands/explain.mjs +207 -1
- package/src/commands/history.mjs +81 -5
- package/src/commands/plan-context-command.mjs +163 -2
- package/src/commands/provenance-command.mjs +86 -17
- package/src/commands/provenance.mjs +60 -0
- package/src/commands/report.mjs +48 -1
- package/src/commands/trajectory.mjs +89 -3
- package/src/fixtures/evolution-lifecycle/workspace.mjs +242 -0
- package/src/governance/adr-registry.mjs +252 -15
- package/src/governance/debt-ledger.mjs +261 -19
- package/src/governance/decision-fitness.mjs +213 -0
- package/src/governance/decision-graph.mjs +483 -0
- package/src/governance/decision-lineage.mjs +250 -0
- package/src/governance/evolution-event.mjs +470 -0
- package/src/governance/evolution-store.mjs +362 -0
- package/src/governance/provenance-record.mjs +150 -0
- package/src/providers/native/model.mjs +18 -4
- package/src/report/adr-text.mjs +109 -4
- package/src/report/change-text.mjs +21 -3
- package/src/report/debt-text.mjs +42 -6
- package/src/report/decisions-text.mjs +164 -0
- package/src/report/delta-text.mjs +36 -1
- package/src/report/evolution-text.mjs +231 -2
- package/src/report/explain-text.mjs +122 -1
- package/src/report/history-text.mjs +9 -3
- package/src/report/plan-context-text.mjs +94 -0
- package/src/report/provenance-text.mjs +67 -1
- package/src/report/report-text.mjs +53 -18
- package/src/report/snapshot-text.mjs +35 -1
- package/src/report/trajectory-text.mjs +30 -1
|
@@ -155,7 +155,13 @@ function fitnessLines(fitness) {
|
|
|
155
155
|
*/
|
|
156
156
|
function decisionLines(decisions) {
|
|
157
157
|
const lines = [surfaceLine("decisions", decisions)];
|
|
158
|
-
|
|
158
|
+
// "does not resolve" is a claim about the REF — that nothing in the
|
|
159
|
+
// workspace answers to it. When the registry itself could not be read, that
|
|
160
|
+
// claim is not one this run established: it could not look. The two get
|
|
161
|
+
// different sentences, for the same reason `unknown` and `not_applicable`
|
|
162
|
+
// do (`count: null` is how the command says the registry was unreadable).
|
|
163
|
+
const registryUnread = decisions.registry.count === null;
|
|
164
|
+
if (!registryUnread) {
|
|
159
165
|
lines.push(
|
|
160
166
|
` ${decisions.registry.count} record${decisions.registry.count === 1 ? "" : "s"} in ` +
|
|
161
167
|
`${decisions.registry.dir}/`,
|
|
@@ -165,29 +171,58 @@ function decisionLines(decisions) {
|
|
|
165
171
|
record.bindings.length > 0
|
|
166
172
|
? `binds ${record.bindings.join(", ")}`
|
|
167
173
|
: "binds nothing — not yet enforceable";
|
|
174
|
+
// The record row is byte-identical to the pre-wave-2 line; the fitness
|
|
175
|
+
// and "stands on" lines below it are additive surface (contract items
|
|
176
|
+
// 3 + F/B), never a rewrite of what a longer-maintained reader expects.
|
|
168
177
|
lines.push(` ${record.id} (${record.status}) ${binds}`);
|
|
178
|
+
if (record.fitness !== undefined) {
|
|
179
|
+
const fit = record.fitness;
|
|
180
|
+
const reasonString =
|
|
181
|
+
typeof fit.reason === "string"
|
|
182
|
+
? ` — ${fit.reason}`
|
|
183
|
+
: fit.verified
|
|
184
|
+
? " — verified true: bound constraints resolve and pass"
|
|
185
|
+
: "";
|
|
186
|
+
lines.push(` fitness: ${fit.level}${reasonString}`);
|
|
187
|
+
}
|
|
188
|
+
if (record.authority) {
|
|
189
|
+
if (record.constraints.length > 0) {
|
|
190
|
+
const refs = record.constraints.map((c) => `${c.kind} ${c.label}`).join(", ");
|
|
191
|
+
lines.push(` stands on: ${refs}`);
|
|
192
|
+
} else {
|
|
193
|
+
lines.push(" stands on: no governed row cites this decision");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
169
196
|
}
|
|
170
197
|
}
|
|
198
|
+
// The citations render even when the registry could not be read: that is
|
|
199
|
+
// exactly the case where "does not resolve" must NOT be claimed and the
|
|
200
|
+
// registry-unreadable sentence must. Skipping them here would print the
|
|
201
|
+
// "unknown" verdict with no citation lines to back it — the silent direction.
|
|
171
202
|
if (decisions.citations.length === 0) {
|
|
172
203
|
lines.push(" no governed row cites a decisionRef");
|
|
173
|
-
|
|
204
|
+
} else {
|
|
205
|
+
for (const citation of decisions.citations) {
|
|
206
|
+
const target =
|
|
207
|
+
citation.resolution === "adr" && citation.adr !== null
|
|
208
|
+
? `${citation.adr.id} (${citation.adr.status})`
|
|
209
|
+
: citation.resolution === "fitness"
|
|
210
|
+
? `${citation.decisionRef} — a fitness rule this law declares`
|
|
211
|
+
: registryUnread
|
|
212
|
+
? `${citation.decisionRef} — unresolved: the decision registry could not be read`
|
|
213
|
+
: `${citation.decisionRef} — does not resolve`;
|
|
214
|
+
lines.push(` ${citation.resolution.padEnd(10)}${citation.label} → ${target}`);
|
|
215
|
+
}
|
|
174
216
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
? `${citation.adr.id} (${citation.adr.status})`
|
|
185
|
-
: citation.resolution === "fitness"
|
|
186
|
-
? `${citation.decisionRef} — a fitness rule this law declares`
|
|
187
|
-
: registryUnread
|
|
188
|
-
? `${citation.decisionRef} — unresolved: the decision registry could not be read`
|
|
189
|
-
: `${citation.decisionRef} — does not resolve`;
|
|
190
|
-
lines.push(` ${citation.resolution.padEnd(10)}${citation.label} → ${target}`);
|
|
217
|
+
if (
|
|
218
|
+
!registryUnread &&
|
|
219
|
+
Array.isArray(decisions.unresolvedDecisionRefs) &&
|
|
220
|
+
decisions.unresolvedDecisionRefs.length > 0
|
|
221
|
+
) {
|
|
222
|
+
lines.push(" unresolved decisionRefs:");
|
|
223
|
+
for (const ref of decisions.unresolvedDecisionRefs) {
|
|
224
|
+
lines.push(` ${ref.kind} ${ref.label} → ${ref.reason}`);
|
|
225
|
+
}
|
|
191
226
|
}
|
|
192
227
|
return lines;
|
|
193
228
|
}
|
|
@@ -111,13 +111,47 @@ export function formatChanges(changes) {
|
|
|
111
111
|
* Classifies one transition into the short "kind" a reader skims for.
|
|
112
112
|
*
|
|
113
113
|
* @param {{architectureChanged: boolean, codeDrift: boolean, policyChanged: boolean|null,
|
|
114
|
+
* policyOneSided: boolean, provenanceChanged: boolean|null,
|
|
114
115
|
* providerChanged: boolean}} transition
|
|
115
|
-
* @returns {string}
|
|
116
116
|
*/
|
|
117
117
|
export function transitionKind(transition) {
|
|
118
118
|
if (transition.architectureChanged) return "architecture";
|
|
119
119
|
if (transition.providerChanged) return "provider";
|
|
120
120
|
if (transition.policyChanged === true) return "policy";
|
|
121
121
|
if (transition.codeDrift) return "code drift";
|
|
122
|
+
// A pair whose policy could not be compared — one side records the law
|
|
123
|
+
// (`policyOneSided`) or neither does while the commit advanced
|
|
124
|
+
// (F-HIST-1) — is never rendered "unchanged": that label claims every
|
|
125
|
+
// comparable signal was compared and equal. The record carries these as
|
|
126
|
+
// fields, never parsed out of its notes.
|
|
127
|
+
if (
|
|
128
|
+
transition.policyChanged === null &&
|
|
129
|
+
(transition.policyOneSided || transition.provenanceChanged === true)
|
|
130
|
+
) {
|
|
131
|
+
return "not comparable";
|
|
132
|
+
}
|
|
122
133
|
return "unchanged";
|
|
123
134
|
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* One transition's canonical evolution classes as a renderable line. Shared
|
|
138
|
+
* by every reporter that renders a transition record (`history` today;
|
|
139
|
+
* `evolution` will render the same record), so the JSON field and its prose
|
|
140
|
+
* cannot disagree about what a classification is — one spelling, one home,
|
|
141
|
+
* beside the sibling `transitionKind` label.
|
|
142
|
+
*
|
|
143
|
+
* The line appears only when the record carries classifications: an
|
|
144
|
+
* unclassified transition is labelled by its kind, and the empty-classification
|
|
145
|
+
* statement — where it applies — is a note on the record, never a second line
|
|
146
|
+
* this helper has to invent.
|
|
147
|
+
*
|
|
148
|
+
* @param {string[]|undefined} classifications The record's `classifications`
|
|
149
|
+
* array; `undefined` when the caller's record predates the field.
|
|
150
|
+
* @returns {string[]} The line, or `[]` when there is nothing to state.
|
|
151
|
+
*/
|
|
152
|
+
export function formatClassifications(classifications) {
|
|
153
|
+
if (!Array.isArray(classifications) || classifications.length === 0) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
return [`classifications: ${classifications.map((c) => sanitize(c)).join(", ")}`];
|
|
157
|
+
}
|
|
@@ -96,7 +96,8 @@ function formatAxis(axis, withChanged) {
|
|
|
96
96
|
* transitions: {count: number, architecture: number, policy: number,
|
|
97
97
|
* provider: number, codeDrift: number, incomparable: number, unchanged: number},
|
|
98
98
|
* disclosures: {policyOneSided: number, provenanceOneSided: number, crossRepo: number},
|
|
99
|
-
* projects: object, edges: object
|
|
99
|
+
* projects: object, edges: object,
|
|
100
|
+
* trends: object|null}, coverage: object}} input
|
|
100
101
|
* @returns {string}
|
|
101
102
|
*/
|
|
102
103
|
export function formatTrajectoryReport({ trajectory, coverage }) {
|
|
@@ -135,6 +136,34 @@ export function formatTrajectoryReport({ trajectory, coverage }) {
|
|
|
135
136
|
`dirty captures ${observations.dirtyProvenance} · with provenance ${observations.withProvenance}`,
|
|
136
137
|
);
|
|
137
138
|
|
|
139
|
+
// The trend facts, from the SAME comparable transitions the signals line
|
|
140
|
+
// counts. `null` prints as n/a — an insufficient or fully-incomparable
|
|
141
|
+
// history never reads as zero change — and the basis line names exactly
|
|
142
|
+
// what the counts are a claim about.
|
|
143
|
+
if (trajectory.trends === null) {
|
|
144
|
+
sections.push(
|
|
145
|
+
trajectory.available
|
|
146
|
+
? "trends n/a — no comparable transition classifications"
|
|
147
|
+
: "trends n/a",
|
|
148
|
+
);
|
|
149
|
+
} else {
|
|
150
|
+
const byClass = trajectory.trends.byClass;
|
|
151
|
+
sections.push(
|
|
152
|
+
`trends CHANGE ${byClass.CHANGE} · DRIFT ${byClass.DRIFT} · VIOLATION ${byClass.VIOLATION} · ` +
|
|
153
|
+
`REPAIR ${byClass.REPAIR} · DECISION_CHANGE ${byClass.DECISION_CHANGE} · ` +
|
|
154
|
+
`violations introduced ${trajectory.trends.violationsIntroduced} · resolved ` +
|
|
155
|
+
`${trajectory.trends.violationsResolved}`,
|
|
156
|
+
);
|
|
157
|
+
sections.push(
|
|
158
|
+
`trends basis ${trajectory.trends.comparableTransitions} comparable transition${
|
|
159
|
+
trajectory.trends.comparableTransitions === 1 ? "" : "s"
|
|
160
|
+
} (${trajectory.trends.basis})`,
|
|
161
|
+
);
|
|
162
|
+
if (typeof trajectory.trends.note === "string" && trajectory.trends.note !== "") {
|
|
163
|
+
sections.push(sanitize(trajectory.trends.note));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
138
167
|
for (const note of coverage.notes) {
|
|
139
168
|
sections.push(sanitize(note));
|
|
140
169
|
}
|