@sdd-method/author-cli 0.14.0 → 0.15.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/lib/catalogue/builders/contract-revisions.js +20 -5
- package/dist/lib/catalogue/builders/data-model.js +8 -3
- package/dist/lib/catalogue/canonical-schema.yaml +1 -1
- package/dist/lib/contracts-availability/index.js +4 -0
- package/dist/lib/metamodel/tql-subset-parser.js +4 -5
- package/dist/lib/structure/intended.js +6 -1
- package/dist/maintain-version.json +2 -2
- package/dist/verify-version.json +2 -2
- package/package.json +2 -2
|
@@ -13,10 +13,17 @@
|
|
|
13
13
|
// a file matching no contract, or several, is a finding and produces no row. `relies_on` and `agrees` are a digest
|
|
14
14
|
// beside `file:`, or a map from each file to its digest beside `files:`. A `relies_on` that no longer matches the
|
|
15
15
|
// file's content is reported: the change relies on a revision that has moved, and is re-agreed.
|
|
16
|
+
//
|
|
17
|
+
// A contract declared outside the SDD has no file here. Its current content is what .sdd/contract-availability.yaml
|
|
18
|
+
// attests (M-ADR 0202): the entry whose path is the contract's spec_path as the declaration writes it, so a fresh clone
|
|
19
|
+
// with no sibling checkout builds the same rows. A contract neither here nor attested has no current revision, and an
|
|
20
|
+
// archived change's revision of it is not classified: retired is a comparison with the current content, never the
|
|
21
|
+
// absence of one.
|
|
16
22
|
import { createHash } from "node:crypto";
|
|
17
23
|
import { readdir, readFile } from "node:fs/promises";
|
|
18
24
|
import { join } from "node:path";
|
|
19
25
|
import { parse as parseYaml } from "yaml";
|
|
26
|
+
import { readAvailability } from "../../contracts-availability/index.js";
|
|
20
27
|
const AGREEMENTS = join("docs", "agreements");
|
|
21
28
|
const DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
22
29
|
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -71,21 +78,29 @@ export async function buildContractRevisions(inputs) {
|
|
|
71
78
|
}
|
|
72
79
|
/**
|
|
73
80
|
* The revision rows, and every contract an archived change deprecated. The second is kept apart because a contract
|
|
74
|
-
* declared outside this SDD
|
|
81
|
+
* declared outside this SDD and not attested has no revision row, yet its version is still deprecated.
|
|
75
82
|
*/
|
|
76
83
|
export async function contractRevisionsAndDeprecations(inputs) {
|
|
77
84
|
const { sddPath, findings } = inputs;
|
|
78
85
|
const rows = new Map();
|
|
79
86
|
const current = new Map(); // contract id -> current digest
|
|
87
|
+
const attested = new Map(); // spec_path -> each digest attested for it
|
|
88
|
+
for (const e of await readAvailability(sddPath)) {
|
|
89
|
+
if (!/^[0-9a-f]{64}$/.test(e.sha256))
|
|
90
|
+
continue;
|
|
91
|
+
attested.set(e.path, (attested.get(e.path) ?? new Set()).add(`sha256:${e.sha256}`));
|
|
92
|
+
}
|
|
80
93
|
for (const c of inputs.contracts) {
|
|
81
94
|
const path = String(c["spec_path"] ?? "");
|
|
82
95
|
if (path === "")
|
|
83
96
|
continue;
|
|
84
97
|
const bytes = await readFile(join(sddPath, path)).catch(() => null);
|
|
85
|
-
|
|
98
|
+
const claims = attested.get(path);
|
|
99
|
+
// Two entries attesting different content for one path say nothing about which is current.
|
|
100
|
+
const digest = bytes !== null ? contentDigest(bytes) : claims?.size === 1 ? [...claims][0] : undefined;
|
|
101
|
+
if (digest === undefined)
|
|
86
102
|
continue;
|
|
87
103
|
const id = String(c["id"]);
|
|
88
|
-
const digest = contentDigest(bytes);
|
|
89
104
|
current.set(id, digest);
|
|
90
105
|
const rid = revisionId(id, digest);
|
|
91
106
|
rows.set(rid, { id: rid, contract_id: id, digest, state: c["status"] === "deprecated" ? "deprecated" : "active", sunset_date: "", change_id: "", source_sdd_id: "", platform_sdd_id: "" });
|
|
@@ -145,14 +160,14 @@ export async function contractRevisionsAndDeprecations(inputs) {
|
|
|
145
160
|
}
|
|
146
161
|
if (state === "agreed")
|
|
147
162
|
rows.set(rid, { id: rid, contract_id: contract, digest: p.agrees, state: "agreed", sunset_date: "", change_id: changeId, source_sdd_id: "", platform_sdd_id: "" });
|
|
148
|
-
else if (!existing)
|
|
163
|
+
else if (!existing && now)
|
|
149
164
|
rows.set(rid, { id: rid, contract_id: contract, digest: p.agrees, state: "retired", sunset_date: "", change_id: changeId, source_sdd_id: "", platform_sdd_id: "" });
|
|
150
165
|
}
|
|
151
166
|
}
|
|
152
167
|
const deprecated = new Map();
|
|
153
168
|
for (const [contract, d] of deprecations) {
|
|
154
169
|
deprecated.set(contract, { sunset: d.sunset, changeId: d.changeId });
|
|
155
|
-
// A contract
|
|
170
|
+
// A contract outside this SDD with no attestation has no revision row to mark.
|
|
156
171
|
const now = current.get(contract);
|
|
157
172
|
const rid = now ? revisionId(contract, now) : undefined;
|
|
158
173
|
const row = rid ? rows.get(rid) : undefined;
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
// `..` (non-identifying) is read as `--`, and reported
|
|
11
11
|
// TypeQL, in the documented subset (M-ADR 0210 §4.2, `data-model.tql`), through the fail-closed parseTypes:
|
|
12
12
|
// an entity's owned attributes (@key, @card), and each binary relation as a relationship between its two roles'
|
|
13
|
-
// players, roles kept.
|
|
14
|
-
//
|
|
13
|
+
// players, roles kept. An end's cardinality is the other player's participation: B plays R:b @card(1..1) puts
|
|
14
|
+
// exactly-one at A's end (each B has exactly one A), and an unannotated plays is zero-or-more (TypeDB's default).
|
|
15
|
+
// A role's own @card is a cardinality invariant on the relationship.
|
|
15
16
|
// The keys become invariants: a primary key (PK, @key) is one unique invariant over its attributes, which are not
|
|
16
17
|
// nullable; a UK is a unique invariant over its attribute. What a parser reads and cannot hold is an `extraction`
|
|
17
18
|
// finding, never a silent skip.
|
|
@@ -194,9 +195,13 @@ export function parseTypeqlDataModel(text, where) {
|
|
|
194
195
|
return false;
|
|
195
196
|
});
|
|
196
197
|
const cards = [...d.relates].flatMap(([role, ann]) => ann.required ? [`each ${label} relates exactly one ${role}`] : ann.optional ? [`each ${label} relates at most one ${role}`] : []);
|
|
198
|
+
const participation = (player, role) => {
|
|
199
|
+
const ann = t.entities.get(player)?.plays.get(`${label}:${role}`);
|
|
200
|
+
return ann?.required ? "exactly-one" : ann?.optional ? "zero-or-one" : "zero-or-more";
|
|
201
|
+
};
|
|
197
202
|
for (const from of playersOf(a)) {
|
|
198
203
|
for (const to of playersOf(b)) {
|
|
199
|
-
relationships.push({ from, to, name: label, fromCardinality:
|
|
204
|
+
relationships.push({ from, to, name: label, fromCardinality: participation(to, b), toCardinality: participation(from, a), fromRole: a, toRole: b, roleCards: cards });
|
|
200
205
|
}
|
|
201
206
|
}
|
|
202
207
|
}
|
|
@@ -721,7 +721,7 @@ enums:
|
|
|
721
721
|
- value: zero-or-one
|
|
722
722
|
- value: one-or-more
|
|
723
723
|
- value: zero-or-more
|
|
724
|
-
description: "How many entities one end of a relationship holds for one entity at the other end (M-ADR 0230 §4.3): a Mermaid ER end marker (|| exactly one, |o or o| zero or one, }| or |{ one or more, }o or o{ zero or more). A TypeQL relationship is
|
|
724
|
+
description: "How many entities one end of a relationship holds for one entity at the other end (M-ADR 0230 §4.3): a Mermaid ER end marker (|| exactly one, |o or o| zero or one, }| or |{ one or more, }o or o{ zero or more). A TypeQL relationship's end is the other player's participation: B plays R:b @card(1..1) makes A's end exactly-one, @card(0..1) zero-or-one, and an unannotated plays zero-or-more; a role's @card is a cardinality invariant (domain_invariants). Replaced entity_relationship_cardinality in schema 6.0.0."
|
|
725
725
|
domain_invariant_kind:
|
|
726
726
|
values:
|
|
727
727
|
- value: unique
|
|
@@ -203,6 +203,10 @@ export function inferKind(ref) {
|
|
|
203
203
|
return "cli";
|
|
204
204
|
return undefined;
|
|
205
205
|
}
|
|
206
|
+
/** The SDD's attested external artefacts (`.sdd/contract-availability.yaml`); none when it has no manifest. */
|
|
207
|
+
export async function readAvailability(repo) {
|
|
208
|
+
return readManifest(join(repo, AVAILABILITY_MANIFEST_REL_PATH));
|
|
209
|
+
}
|
|
206
210
|
async function readManifest(abs) {
|
|
207
211
|
if (!existsSync(abs))
|
|
208
212
|
return [];
|
|
@@ -175,11 +175,10 @@ export function parseTypes(text, fileLabel = "catalogue-schema.tql", options = {
|
|
|
175
175
|
if (decl.plays.has(w[1]))
|
|
176
176
|
throw new ParseError(`${where}: ${lab} plays ${w[1]} twice`);
|
|
177
177
|
const ann = parseAnnotations(w.slice(2), where);
|
|
178
|
-
// A plays clause's cardinality constrains participation,
|
|
179
|
-
//
|
|
180
|
-
if (ann.key
|
|
181
|
-
throw new ParseError(`${where}: ${lab} plays ${w[1]}
|
|
182
|
-
}
|
|
178
|
+
// A plays clause's cardinality constrains participation, and is kept: a reader that can hold it does, and a
|
|
179
|
+
// projection that cannot refuses it there (schema-projection), never here, so no reader drops it silently.
|
|
180
|
+
if (ann.key)
|
|
181
|
+
throw new ParseError(`${where}: ${lab} plays ${w[1]} @key; a role is not a key`);
|
|
183
182
|
decl.plays.set(w[1], ann);
|
|
184
183
|
}
|
|
185
184
|
else if (head === "relates") {
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// a source-built row with a change_id that change
|
|
9
9
|
// a contract row the change whose record agrees its file as new
|
|
10
10
|
// a row naming such a contract that change
|
|
11
|
+
// a row built from the change's files that change (its spec_path lies in openspec/changes/<change>/, as a
|
|
12
|
+
// requirement the change ADDS does)
|
|
11
13
|
// Pure but for reading agreement records.
|
|
12
14
|
import { readdir, readFile } from "node:fs/promises";
|
|
13
15
|
import { join } from "node:path";
|
|
@@ -125,7 +127,7 @@ export class Additions {
|
|
|
125
127
|
out.add(k.slice(CONTRACTS.length + 1));
|
|
126
128
|
return out;
|
|
127
129
|
}
|
|
128
|
-
/** The open changes a source-built row belongs to: its change_id, or a new contract it is or names. */
|
|
130
|
+
/** The open changes a source-built row belongs to: its change_id, the change whose files it is built from, or a new contract it is or names. */
|
|
129
131
|
ownersOf(schema, catalogue, row, sddId) {
|
|
130
132
|
const out = new Set();
|
|
131
133
|
const changeId = cell(row["change_id"]);
|
|
@@ -134,6 +136,9 @@ export class Additions {
|
|
|
134
136
|
if (this.open.has(name))
|
|
135
137
|
out.add(name);
|
|
136
138
|
}
|
|
139
|
+
const from = /^openspec\/changes\/([^/]+)\//.exec(cell(row["spec_path"]))?.[1];
|
|
140
|
+
if (from !== undefined && this.open.has(from))
|
|
141
|
+
out.add(from);
|
|
137
142
|
const contractOwner = (id) => {
|
|
138
143
|
const by = this.adder.get(elementKey(CONTRACTS, id));
|
|
139
144
|
if (by !== undefined)
|
package/dist/verify-version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdd-method/author-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"sddCliVersion": "5.
|
|
3
|
+
"version": "0.15.0",
|
|
4
|
+
"sddCliVersion": "5.2.0",
|
|
5
5
|
"description": "Re-author one SDD's content — scaffold, plan, contracts, conformance, api-surface, snapshots, briefings, catalogue check — plus every read-only verify verb. Ships only the code those verbs reach; it cannot sync the baseline or rebuild the catalogues. Maintenance is in @sdd-method/maintain-cli, administration in @sdd-method/sdd-cli.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"type": "module",
|