@sdd-method/verify-cli 1.4.0 → 2.0.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/canonical-schema.yaml +160 -7
- package/dist/lib/catalogue/rubric-coverage-map.yaml +36 -0
- package/dist/lib/catalogue/schema-compat.js +1 -1
- package/dist/lib/validate/behaviour-store.js +351 -0
- package/dist/lib/validate/dispatch.js +18 -0
- package/dist/lib/validate/plan.js +2 -0
- package/dist/maintain-version.json +2 -2
- package/dist/verify-version.json +2 -2
- package/package.json +4 -6
- package/scripts/validate-app-dependencies.sh +30 -22
- package/scripts/validate-integration-docs.sh +1 -1
- package/LICENSE +0 -201
- package/NOTICE +0 -17
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# Emitted by sdd-cli's YAML emitter with no line folding; the three header scalars are quoted for the
|
|
8
8
|
# textual readers downstream. The one deliberate difference from the authored form is schema_status:
|
|
9
9
|
# the authored form says canonical; a projection never does.
|
|
10
|
-
schema_version: "
|
|
10
|
+
schema_version: "4.0.0"
|
|
11
11
|
schema_owner: "sdd-method"
|
|
12
12
|
schema_status: "generated"
|
|
13
13
|
enums:
|
|
@@ -389,7 +389,7 @@ enums:
|
|
|
389
389
|
description: M-ADR 0159 entity persistence kind. Bound at 3.13.0 from the column descriptions; measured against both reference estates before binding (M-ADR 0205 §4.4).
|
|
390
390
|
required_stability:
|
|
391
391
|
values:
|
|
392
|
-
- value:
|
|
392
|
+
- value: stable
|
|
393
393
|
- value: provisional
|
|
394
394
|
- value: unknown
|
|
395
395
|
description: "M-ADR 0204: the stability a consumer requires of a target; external | legacy live in target_locus, not here. Bound at 3.13.0 from the column descriptions; measured against both reference estates before binding (M-ADR 0205 §4.4)."
|
|
@@ -613,6 +613,35 @@ enums:
|
|
|
613
613
|
- value: generated-from
|
|
614
614
|
description: Its content was produced from the other repository's templates.
|
|
615
615
|
description: How one repository depends on another's code (M-ADR 0217 §4.1).
|
|
616
|
+
requirement_stage:
|
|
617
|
+
values:
|
|
618
|
+
- value: living
|
|
619
|
+
description: "In the living spec: shipped behaviour."
|
|
620
|
+
- value: agreed
|
|
621
|
+
description: Added by an open change whose agreement record is agreed; not yet shipped.
|
|
622
|
+
description: Where a catalogued requirement stands (M-ADR 0221 §4.4).
|
|
623
|
+
requirement_change_kind:
|
|
624
|
+
values:
|
|
625
|
+
- value: baseline
|
|
626
|
+
description: Recorded existing behaviour through the baseline gate.
|
|
627
|
+
- value: change
|
|
628
|
+
description: A delta agreed, built and judged through the change gate.
|
|
629
|
+
description: What kind of archived change a lineage row names (M-ADR 0222 §4.3).
|
|
630
|
+
requirement_operation:
|
|
631
|
+
values:
|
|
632
|
+
- value: ADDED
|
|
633
|
+
- value: MODIFIED
|
|
634
|
+
- value: RENAMED
|
|
635
|
+
description: How a change's delta treated a requirement that is current now (a removed requirement has no row).
|
|
636
|
+
requirement_status_value:
|
|
637
|
+
values:
|
|
638
|
+
- value: agreed
|
|
639
|
+
description: In an agreed, open change.
|
|
640
|
+
- value: declared-shipped
|
|
641
|
+
description: Its change archived on a passing judge, with this repository's task group complete.
|
|
642
|
+
- value: observed
|
|
643
|
+
description: Independent evidence from this repository's code matches (a baseline's characterization tests or a reverse snapshot).
|
|
644
|
+
description: A requirement's status in one implementing repository (M-ADR 0221 §4.5).
|
|
616
645
|
catalogues:
|
|
617
646
|
domains:
|
|
618
647
|
primary_key: id
|
|
@@ -1276,7 +1305,7 @@ catalogues:
|
|
|
1276
1305
|
- name: contract_provenance
|
|
1277
1306
|
type: string
|
|
1278
1307
|
nullable: true
|
|
1279
|
-
description: "How this capability's declared contract SET was produced, from `traceability.contracts.provenance`. hand-authored | reverse-extracted | generated — the vocabulary the method already ships for `authoring-mode`, reused deliberately so provenance has one vocabulary and not two. Empty means undeclared, and undeclared reads as `hand-authored` by convention. Added at 3.5.0. ⚠ STRICTLY ORTHOGONAL to the contract-set maturity ladder (`traceability.contracts.status`: planned | provisional | partial | complete |
|
|
1308
|
+
description: "How this capability's declared contract SET was produced, from `traceability.contracts.provenance`. hand-authored | reverse-extracted | generated — the vocabulary the method already ships for `authoring-mode`, reused deliberately so provenance has one vocabulary and not two. Empty means undeclared, and undeclared reads as `hand-authored` by convention. Added at 3.5.0. ⚠ STRICTLY ORTHOGONAL to the contract-set maturity ladder (`traceability.contracts.status`: planned | provisional | partial | complete | stable | none): provenance never constrains status and status never constrains provenance, and `reverse-extracted` + `complete` is the legitimate pair that motivated the field. Evidence: 2 reverse-estate manifests had written `reverse-extracted` INTO contracts.status, where it is invalid, because a complete reverse-derived contract surface had no way to say both true things at once and the author picked the provenance one. A rule here that constrained maturity would re-create exactly that defect. ⚠ Note the asymmetry this column exposes and does not fix: the maturity ladder it is orthogonal to has NO catalogue column at all — `traceability.contracts.status` is validator-enforced but uncatalogued, so it is queryable by no consumer. That is the authored-but-uncatalogued class (a block with no column has no automated reader, so nothing can notice when it is absent or wrong); this column is given one precisely so the provenance half does not join it. Giving the ladder a column is a candidate follow-on, not done here."
|
|
1280
1309
|
- name: source
|
|
1281
1310
|
type: string
|
|
1282
1311
|
nullable: false
|
|
@@ -1523,7 +1552,7 @@ catalogues:
|
|
|
1523
1552
|
- name: kind
|
|
1524
1553
|
type: string
|
|
1525
1554
|
nullable: false
|
|
1526
|
-
description: rest | graphql | rpc | event | authz. rpc covers Proto/gRPC; event covers Avro schemas; authz covers authorization/policy schemas (SpiceDB .zed and equivalents), added because an adopter contract
|
|
1555
|
+
description: rest | graphql | rpc | event | authz. rpc covers Proto/gRPC; event covers Avro schemas; authz covers authorization/policy schemas (SpiceDB .zed and equivalents), added because an adopter contract agreement gate certified .zed schemas as contracts while no kind could represent them.
|
|
1527
1556
|
- name: version
|
|
1528
1557
|
type: string
|
|
1529
1558
|
nullable: true
|
|
@@ -1888,7 +1917,7 @@ catalogues:
|
|
|
1888
1917
|
- name: version
|
|
1889
1918
|
type: string
|
|
1890
1919
|
nullable: true
|
|
1891
|
-
description: Optional explicit version; null until the domain model is formally versioned (typically
|
|
1920
|
+
description: Optional explicit version; null until the domain model is formally versioned (typically when a contract is first pinned).
|
|
1892
1921
|
- name: evidence_class
|
|
1893
1922
|
type: string
|
|
1894
1923
|
nullable: true
|
|
@@ -2142,7 +2171,7 @@ catalogues:
|
|
|
2142
2171
|
- name: required_stability
|
|
2143
2172
|
type: string
|
|
2144
2173
|
nullable: true
|
|
2145
|
-
description: "provisional |
|
|
2174
|
+
description: "provisional | stable | unknown. The consuming SDD's expectation of contract stability, carried verbatim from app-platform-dependencies.yaml. Not a bound enum: an out-of-vocabulary value is carried, not rejected, and is reported by validate-app-dependencies.sh."
|
|
2146
2175
|
description: "Edge: product depends on / consumes service. Forward sources from app-platform-dependencies.yaml. service_id is external because the consuming product (an application-product SDD) references a service owned by the platform SDD."
|
|
2147
2176
|
foreign_keys:
|
|
2148
2177
|
- column: product_id
|
|
@@ -2930,7 +2959,7 @@ catalogues:
|
|
|
2930
2959
|
- name: required_stability
|
|
2931
2960
|
type: string
|
|
2932
2961
|
nullable: true
|
|
2933
|
-
description: "provisional |
|
|
2962
|
+
description: "provisional | stable | unknown. Optional; the consuming SDD's expectation of contract stability — and only that: where the target lives and what lifecycle its implementation is in are separate concerns that do not belong in this field. Not a bound enum: an out-of-vocabulary value is carried, not rejected, and is reported by validate-app-dependencies.sh."
|
|
2934
2963
|
- name: contract_path
|
|
2935
2964
|
type: string
|
|
2936
2965
|
nullable: true
|
|
@@ -4115,3 +4144,127 @@ catalogues:
|
|
|
4115
4144
|
external: true
|
|
4116
4145
|
column_enums:
|
|
4117
4146
|
kind: enums.repo_dependency_kind
|
|
4147
|
+
requirements:
|
|
4148
|
+
primary_key: id
|
|
4149
|
+
columns:
|
|
4150
|
+
- name: id
|
|
4151
|
+
type: string
|
|
4152
|
+
nullable: false
|
|
4153
|
+
description: <feature_id>::<requirement name>. The name is unique within its spec and stable across changes (M-ADR 0221 §4.7).
|
|
4154
|
+
- name: feature_id
|
|
4155
|
+
type: string
|
|
4156
|
+
nullable: false
|
|
4157
|
+
description: "The catalogue feature whose living spec holds the requirement: the spec's directory name under openspec/specs/."
|
|
4158
|
+
- name: name
|
|
4159
|
+
type: string
|
|
4160
|
+
nullable: false
|
|
4161
|
+
description: The requirement's name as the spec heads it; automated tests carry it.
|
|
4162
|
+
- name: stage
|
|
4163
|
+
type: string
|
|
4164
|
+
nullable: false
|
|
4165
|
+
description: "Bound to requirement_stage: living (in the living spec) or agreed (added by an open change whose agreement record is agreed)."
|
|
4166
|
+
- name: spec_path
|
|
4167
|
+
type: string
|
|
4168
|
+
nullable: false
|
|
4169
|
+
description: "The file that holds the requirement: the living spec, or the open change's delta."
|
|
4170
|
+
- name: scenario_count
|
|
4171
|
+
type: integer
|
|
4172
|
+
nullable: false
|
|
4173
|
+
description: Scenarios under the requirement in that file.
|
|
4174
|
+
- name: source_sdd_id
|
|
4175
|
+
type: string
|
|
4176
|
+
nullable: true
|
|
4177
|
+
description: Originating SDD (stamped at aggregation; empty in a per-SDD build).
|
|
4178
|
+
- name: platform_sdd_id
|
|
4179
|
+
type: string
|
|
4180
|
+
nullable: true
|
|
4181
|
+
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
4182
|
+
description: "A requirement in the SDD's behaviour store (M-ADR 0221): one row per requirement of a living spec, keyed by the catalogue feature the spec describes, and per requirement an open agreed change adds. Projected from openspec/ at the SDD's commit; never authored here."
|
|
4183
|
+
foreign_keys:
|
|
4184
|
+
- column: feature_id
|
|
4185
|
+
references: features.id
|
|
4186
|
+
nullable: false
|
|
4187
|
+
column_enums:
|
|
4188
|
+
stage: enums.requirement_stage
|
|
4189
|
+
requirement_lineage:
|
|
4190
|
+
primary_key: id
|
|
4191
|
+
columns:
|
|
4192
|
+
- name: id
|
|
4193
|
+
type: string
|
|
4194
|
+
nullable: false
|
|
4195
|
+
description: <requirement_id>__<change_id>.
|
|
4196
|
+
- name: requirement_id
|
|
4197
|
+
type: string
|
|
4198
|
+
nullable: false
|
|
4199
|
+
- name: change_id
|
|
4200
|
+
type: string
|
|
4201
|
+
nullable: false
|
|
4202
|
+
description: The archived change's directory name (its archive date prefix included).
|
|
4203
|
+
- name: change_kind
|
|
4204
|
+
type: string
|
|
4205
|
+
nullable: false
|
|
4206
|
+
description: "Bound to requirement_change_kind: baseline or change."
|
|
4207
|
+
- name: operation
|
|
4208
|
+
type: string
|
|
4209
|
+
nullable: false
|
|
4210
|
+
description: "Bound to requirement_operation: how the change's delta treated the requirement."
|
|
4211
|
+
- name: archived_on
|
|
4212
|
+
type: string
|
|
4213
|
+
nullable: false
|
|
4214
|
+
description: The archive date (YYYY-MM-DD) from the change's directory name.
|
|
4215
|
+
- name: source_sdd_id
|
|
4216
|
+
type: string
|
|
4217
|
+
nullable: true
|
|
4218
|
+
description: Originating SDD (stamped at aggregation; empty in a per-SDD build).
|
|
4219
|
+
- name: platform_sdd_id
|
|
4220
|
+
type: string
|
|
4221
|
+
nullable: true
|
|
4222
|
+
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
4223
|
+
description: "Annotation: an archived change that made a current requirement what it is (M-ADR 0221 §4.6), in archive order. A baseline change (one carrying baseline-evidence.md) records existing behaviour; any other change carries a verdict."
|
|
4224
|
+
foreign_keys:
|
|
4225
|
+
- column: requirement_id
|
|
4226
|
+
references: requirements.id
|
|
4227
|
+
nullable: false
|
|
4228
|
+
column_enums:
|
|
4229
|
+
change_kind: enums.requirement_change_kind
|
|
4230
|
+
operation: enums.requirement_operation
|
|
4231
|
+
requirement_status:
|
|
4232
|
+
primary_key: id
|
|
4233
|
+
columns:
|
|
4234
|
+
- name: id
|
|
4235
|
+
type: string
|
|
4236
|
+
nullable: false
|
|
4237
|
+
description: <requirement_id>__<repo_id>.
|
|
4238
|
+
- name: requirement_id
|
|
4239
|
+
type: string
|
|
4240
|
+
nullable: false
|
|
4241
|
+
- name: repo_id
|
|
4242
|
+
type: string
|
|
4243
|
+
nullable: false
|
|
4244
|
+
description: "The implementing repository: a work order's repository, or a repository a baseline was observed in."
|
|
4245
|
+
- name: status
|
|
4246
|
+
type: string
|
|
4247
|
+
nullable: false
|
|
4248
|
+
description: Bound to requirement_status_value.
|
|
4249
|
+
- name: basis
|
|
4250
|
+
type: string
|
|
4251
|
+
nullable: false
|
|
4252
|
+
description: "The change that produced the status: the open change's or the archived change's directory name."
|
|
4253
|
+
- name: source_sdd_id
|
|
4254
|
+
type: string
|
|
4255
|
+
nullable: true
|
|
4256
|
+
description: Originating SDD (stamped at aggregation; empty in a per-SDD build).
|
|
4257
|
+
- name: platform_sdd_id
|
|
4258
|
+
type: string
|
|
4259
|
+
nullable: true
|
|
4260
|
+
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
4261
|
+
description: "Edge: a requirement's status in a repository that implements it (M-ADR 0221 §4.5; M-ADR 0223 §4.7). Derived, never authored: agreed from an open agreed change's work orders, declared-shipped from an archived change's PASS verdict and its work orders, observed from a baseline's evidence. The latest event in archive order wins."
|
|
4262
|
+
foreign_keys:
|
|
4263
|
+
- column: requirement_id
|
|
4264
|
+
references: requirements.id
|
|
4265
|
+
nullable: false
|
|
4266
|
+
- column: repo_id
|
|
4267
|
+
references: repositories.id
|
|
4268
|
+
nullable: false
|
|
4269
|
+
column_enums:
|
|
4270
|
+
status: enums.requirement_status_value
|
|
@@ -869,3 +869,39 @@ coverage:
|
|
|
869
869
|
kind: enum
|
|
870
870
|
scoring: not-applicable
|
|
871
871
|
reason: closed vocabulary, not an artefact
|
|
872
|
+
requirements:
|
|
873
|
+
kind: entity
|
|
874
|
+
scoring: not-applicable
|
|
875
|
+
reason: projected from the behaviour store, whose baseline and change gates hold its quality (M-ADR 0222)
|
|
876
|
+
relationships:
|
|
877
|
+
feature_id:
|
|
878
|
+
class: membership
|
|
879
|
+
impact_flow: toward-target
|
|
880
|
+
requirement_lineage:
|
|
881
|
+
kind: annotation
|
|
882
|
+
scoring: derived-edge
|
|
883
|
+
class: lineage
|
|
884
|
+
impact_flow: none
|
|
885
|
+
reason: derived from the archived changes scored by their own judge
|
|
886
|
+
requirement_status:
|
|
887
|
+
kind: edge
|
|
888
|
+
scoring: derived-edge
|
|
889
|
+
class: realisation
|
|
890
|
+
impact_flow: both
|
|
891
|
+
reason: derived from agreement records, verdicts and baseline evidence
|
|
892
|
+
requirement_stage:
|
|
893
|
+
kind: enum
|
|
894
|
+
scoring: not-applicable
|
|
895
|
+
reason: closed vocabulary, not an artefact
|
|
896
|
+
requirement_change_kind:
|
|
897
|
+
kind: enum
|
|
898
|
+
scoring: not-applicable
|
|
899
|
+
reason: closed vocabulary, not an artefact
|
|
900
|
+
requirement_operation:
|
|
901
|
+
kind: enum
|
|
902
|
+
scoring: not-applicable
|
|
903
|
+
reason: closed vocabulary, not an artefact
|
|
904
|
+
requirement_status_value:
|
|
905
|
+
kind: enum
|
|
906
|
+
scoring: not-applicable
|
|
907
|
+
reason: closed vocabulary, not an artefact
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// Behaviour-store checks (M-ADR 0221, 0222, 0223): the gates OpenSpec itself does not enforce.
|
|
2
|
+
//
|
|
3
|
+
// An SDD's behaviour store is an OpenSpec project; in the inside-SDD shape it is `openspec/` at the
|
|
4
|
+
// SDD root, with one agreement record per change at `docs/agreements/<change>.yaml`
|
|
5
|
+
// (agreement-record-schema.yaml). OpenSpec checks the format of specs and deltas. It does not check
|
|
6
|
+
// that a change was agreed before it was built, that the requirements it modifies stayed as they
|
|
7
|
+
// were agreed, or that an archived change was judged. These are those checks:
|
|
8
|
+
//
|
|
9
|
+
// 1. records: every agreement record has the shape the checks below read.
|
|
10
|
+
// 2. agreement: an open change with any completed task has an agreed record and a Gate 0 that
|
|
11
|
+
// ends CLEAR (the pre-apply check, M-ADR 0223 §4.3).
|
|
12
|
+
// 3. stale: for each open, agreed change that MODIFIES, REMOVES or RENAMES requirements, the
|
|
13
|
+
// targeted living specs are unchanged since the agreed commit (M-ADR 0223 §4.5).
|
|
14
|
+
// With no such targets the check is vacuous, and says so.
|
|
15
|
+
// 4. verdicts: every archived change except a baseline has a verdict.md and a record whose
|
|
16
|
+
// verdict is PASS and names it (M-ADR 0223 §4.6).
|
|
17
|
+
//
|
|
18
|
+
// A sibling-repository store (M-ADR 0221 §4.2) is checked in that repository, not here.
|
|
19
|
+
//
|
|
20
|
+
// OPENSPEC'S OWN VALIDATION is the separate validator `behaviour-store-openspec`, because it needs
|
|
21
|
+
// the OpenSpec CLI at OPENSPEC_VERSION (the Method Validation module installs it). Without a store
|
|
22
|
+
// both report [N/A], an empty population, never a pass.
|
|
23
|
+
import { execFile } from "node:child_process";
|
|
24
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
25
|
+
import { basename, join } from "node:path";
|
|
26
|
+
import { promisify } from "node:util";
|
|
27
|
+
import { parseDocument } from "yaml";
|
|
28
|
+
const execFileAsync = promisify(execFile);
|
|
29
|
+
/** The OpenSpec release the method depends on (M-ADR 0223 ruling 2). The module installs exactly this. */
|
|
30
|
+
export const OPENSPEC_VERSION = "1.14.3";
|
|
31
|
+
/** Telemetry off for every OpenSpec run the method makes. */
|
|
32
|
+
export const OPENSPEC_ENV = { OPENSPEC_TELEMETRY: "0", DO_NOT_TRACK: "1" };
|
|
33
|
+
export const STORE_DIR = "openspec";
|
|
34
|
+
export const AGREEMENTS_DIR = "docs/agreements";
|
|
35
|
+
const AGREEMENT_STATUSES = ["proposed", "agreed", "superseded", "withdrawn"];
|
|
36
|
+
const COMMIT_RE = /^[0-9a-f]{7,40}$/;
|
|
37
|
+
// OpenSpec counts a task done when its box holds only `x`, either case, any spacing.
|
|
38
|
+
const TASK_DONE_RE = /^\s*-\s*\[\s*[xX]\s*\]/m;
|
|
39
|
+
const TARGET_HEADING_RE = /^##\s+(MODIFIED|REMOVED|RENAMED)\s+Requirements\b/m;
|
|
40
|
+
async function exists(p) {
|
|
41
|
+
try {
|
|
42
|
+
await access(p);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function dirs(p) {
|
|
50
|
+
try {
|
|
51
|
+
return (await readdir(p, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function readText(p) {
|
|
58
|
+
try {
|
|
59
|
+
return await readFile(p, "utf8");
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Read and shape-check one agreement record. Returns the record, or the problems found. */
|
|
66
|
+
export function parseAgreementRecord(file, text) {
|
|
67
|
+
const problems = [];
|
|
68
|
+
const doc = parseDocument(text);
|
|
69
|
+
if (doc.errors.length > 0) {
|
|
70
|
+
return { problems: doc.errors.map((e) => `${file}: does not parse: ${e.message.split("\n")[0]}`) };
|
|
71
|
+
}
|
|
72
|
+
const root = doc.toJS();
|
|
73
|
+
const r = root?.agreement_record;
|
|
74
|
+
if (!r || typeof r !== "object")
|
|
75
|
+
return { problems: [`${file}: no top-level agreement_record`] };
|
|
76
|
+
const expected = basename(file).replace(/\.ya?ml$/, "");
|
|
77
|
+
if (r.change !== expected)
|
|
78
|
+
problems.push(`${file}: change is ${JSON.stringify(r.change)}, expected "${expected}" (the file name)`);
|
|
79
|
+
const status = typeof r.status === "string" ? r.status : "";
|
|
80
|
+
if (!AGREEMENT_STATUSES.includes(status)) {
|
|
81
|
+
problems.push(`${file}: status ${JSON.stringify(r.status)} is not one of ${AGREEMENT_STATUSES.join(" | ")}`);
|
|
82
|
+
}
|
|
83
|
+
const sdd = (r.sdd ?? {});
|
|
84
|
+
const gates = Array.isArray(r.gates) ? r.gates : [];
|
|
85
|
+
const gate0 = gates.find((g) => g && g.name === "gate-0");
|
|
86
|
+
if (status === "agreed") {
|
|
87
|
+
if (r.agreed_on === undefined || r.agreed_on === null)
|
|
88
|
+
problems.push(`${file}: agreed, but no agreed_on`);
|
|
89
|
+
if (typeof r.approved_by !== "string" || r.approved_by.trim() === "")
|
|
90
|
+
problems.push(`${file}: agreed, but no approved_by`);
|
|
91
|
+
if (typeof sdd.commit !== "string") {
|
|
92
|
+
problems.push(`${file}: sdd.commit must be a quoted string, found ${sdd.commit === undefined ? "nothing" : `${typeof sdd.commit} ${String(sdd.commit)}`}` +
|
|
93
|
+
(typeof sdd.commit === "number" ? " (unquoted, YAML parsed the SHA as a number)" : ""));
|
|
94
|
+
}
|
|
95
|
+
else if (!COMMIT_RE.test(sdd.commit)) {
|
|
96
|
+
problems.push(`${file}: sdd.commit "${sdd.commit}" is not 7-40 hex characters`);
|
|
97
|
+
}
|
|
98
|
+
if (!gate0)
|
|
99
|
+
problems.push(`${file}: agreed, but no gates entry named gate-0`);
|
|
100
|
+
else if (gate0.result !== "CLEAR")
|
|
101
|
+
problems.push(`${file}: agreed, but gate-0 result is ${JSON.stringify(gate0.result)}, not CLEAR`);
|
|
102
|
+
}
|
|
103
|
+
// Every commit a record pins must survive parsing as a string. YAML 1.2 reads an unquoted
|
|
104
|
+
// all-digit abbreviation as an integer and one shaped like `1234e567` or `123e456` as a float
|
|
105
|
+
// (Infinity), and the pin is then lost; checking the PARSED type is the only test that catches
|
|
106
|
+
// both, since a pattern applied after coercion sees "Infinity" or a number.
|
|
107
|
+
for (const [path, value] of commitFields(r)) {
|
|
108
|
+
if (path === "sdd.commit")
|
|
109
|
+
continue;
|
|
110
|
+
if (typeof value !== "string") {
|
|
111
|
+
problems.push(`${file}: ${path} must be a quoted string, found ${typeof value} ${String(value)} (unquoted, YAML parsed the SHA as a number)`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const verdict = r.verdict && typeof r.verdict === "object" ? r.verdict : undefined;
|
|
115
|
+
if (problems.length > 0)
|
|
116
|
+
return { problems };
|
|
117
|
+
return {
|
|
118
|
+
record: {
|
|
119
|
+
file,
|
|
120
|
+
change: expected,
|
|
121
|
+
status,
|
|
122
|
+
...(typeof sdd.commit === "string" ? { commit: sdd.commit } : {}),
|
|
123
|
+
...(typeof gate0?.result === "string" ? { gate0: gate0.result } : {}),
|
|
124
|
+
...(verdict ? { verdict } : {}),
|
|
125
|
+
},
|
|
126
|
+
problems,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Every pinned commit in a record: any key named `commit` or ending `_commit`, and each value under verdict.merged and current. */
|
|
130
|
+
function commitFields(r) {
|
|
131
|
+
const out = [];
|
|
132
|
+
const walk = (v, path, pinMap) => {
|
|
133
|
+
if (Array.isArray(v))
|
|
134
|
+
v.forEach((x, i) => walk(x, `${path}[${i}]`, false));
|
|
135
|
+
else if (v && typeof v === "object") {
|
|
136
|
+
for (const [k, x] of Object.entries(v)) {
|
|
137
|
+
const p = path === "" ? k : `${path}.${k}`;
|
|
138
|
+
const isPin = pinMap || k === "commit" || k.endsWith("_commit");
|
|
139
|
+
if (isPin && (x === null || typeof x !== "object"))
|
|
140
|
+
out.push([p, x]);
|
|
141
|
+
else
|
|
142
|
+
walk(x, p, p === "verdict.merged" || p === "current");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
walk(r, "", false);
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
function lastGateLine(text) {
|
|
150
|
+
const lines = text.split("\n").map((l) => l.trim()).filter((l) => l !== "");
|
|
151
|
+
return lines.at(-1) ?? "";
|
|
152
|
+
}
|
|
153
|
+
async function gitDiffQuiet(repo, commit, path) {
|
|
154
|
+
try {
|
|
155
|
+
await execFileAsync("git", ["-C", repo, "cat-file", "-e", `${commit}^{commit}`]);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return "unresolved";
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
await execFileAsync("git", ["-C", repo, "diff", "--quiet", commit, "HEAD", "--", path]);
|
|
162
|
+
return "same";
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
return err.code === 1 ? "changed" : "unresolved";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** The living specs an open change's deltas modify, remove or rename: openspec/specs/<capability>/spec.md. */
|
|
169
|
+
async function targetedSpecs(repo, changeDir) {
|
|
170
|
+
const out = [];
|
|
171
|
+
const walk = async (rel) => {
|
|
172
|
+
const abs = join(repo, changeDir, "specs", rel);
|
|
173
|
+
let entries;
|
|
174
|
+
try {
|
|
175
|
+
entries = await readdir(abs, { withFileTypes: true });
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
for (const e of entries) {
|
|
181
|
+
const childRel = rel === "" ? e.name : `${rel}/${e.name}`;
|
|
182
|
+
if (e.isDirectory())
|
|
183
|
+
await walk(childRel);
|
|
184
|
+
else if (e.name === "spec.md") {
|
|
185
|
+
const text = (await readText(join(abs, e.name))) ?? "";
|
|
186
|
+
if (TARGET_HEADING_RE.test(text))
|
|
187
|
+
out.push(`${STORE_DIR}/specs/${rel}/spec.md`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
await walk("");
|
|
192
|
+
return out.sort();
|
|
193
|
+
}
|
|
194
|
+
export async function checkBehaviourStore(repo) {
|
|
195
|
+
if (!(await exists(join(repo, STORE_DIR)))) {
|
|
196
|
+
return { exitCode: 0, stdout: `[N/A] no behaviour store (${STORE_DIR}/) in this repository\n` };
|
|
197
|
+
}
|
|
198
|
+
const lines = [];
|
|
199
|
+
const failures = [];
|
|
200
|
+
// 1. records
|
|
201
|
+
const records = new Map();
|
|
202
|
+
let recordFiles = [];
|
|
203
|
+
try {
|
|
204
|
+
recordFiles = (await readdir(join(repo, AGREEMENTS_DIR))).filter((f) => /\.ya?ml$/.test(f)).sort();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
recordFiles = [];
|
|
208
|
+
}
|
|
209
|
+
for (const f of recordFiles) {
|
|
210
|
+
const rel = `${AGREEMENTS_DIR}/${f}`;
|
|
211
|
+
const { record, problems } = parseAgreementRecord(rel, (await readText(join(repo, rel))) ?? "");
|
|
212
|
+
failures.push(...problems);
|
|
213
|
+
if (record)
|
|
214
|
+
records.set(record.change, record);
|
|
215
|
+
}
|
|
216
|
+
lines.push(`records: ${recordFiles.length} agreement record(s), ${recordFiles.length - records.size} malformed`);
|
|
217
|
+
// 2 and 3. open changes
|
|
218
|
+
const open = (await dirs(join(repo, STORE_DIR, "changes"))).filter((d) => d !== "archive");
|
|
219
|
+
let started = 0;
|
|
220
|
+
let staleChecked = 0;
|
|
221
|
+
for (const change of open) {
|
|
222
|
+
const dir = `${STORE_DIR}/changes/${change}`;
|
|
223
|
+
const tasks = await readText(join(repo, dir, "tasks.md"));
|
|
224
|
+
const record = records.get(change);
|
|
225
|
+
if (tasks !== null && TASK_DONE_RE.test(tasks)) {
|
|
226
|
+
started++;
|
|
227
|
+
if (!record)
|
|
228
|
+
failures.push(`${dir}: has completed tasks but no agreement record (${AGREEMENTS_DIR}/${change}.yaml)`);
|
|
229
|
+
else if (record.status !== "agreed")
|
|
230
|
+
failures.push(`${dir}: has completed tasks but its agreement record is ${record.status}, not agreed`);
|
|
231
|
+
const gate = await readText(join(repo, dir, "gate-0.md"));
|
|
232
|
+
if (gate === null)
|
|
233
|
+
failures.push(`${dir}: has completed tasks but no gate-0.md`);
|
|
234
|
+
else if (lastGateLine(gate) !== "GATE: CLEAR")
|
|
235
|
+
failures.push(`${dir}: has completed tasks but gate-0.md ends "${lastGateLine(gate)}", not "GATE: CLEAR"`);
|
|
236
|
+
}
|
|
237
|
+
if (record?.status === "agreed" && record.commit) {
|
|
238
|
+
for (const spec of await targetedSpecs(repo, dir)) {
|
|
239
|
+
staleChecked++;
|
|
240
|
+
const r = await gitDiffQuiet(repo, record.commit, spec);
|
|
241
|
+
if (r === "changed")
|
|
242
|
+
failures.push(`${dir}: STALE: ${spec} changed since the agreed commit ${record.commit}`);
|
|
243
|
+
if (r === "unresolved")
|
|
244
|
+
failures.push(`${dir}: stale check unresolved: commit ${record.commit} is not in this clone's history (a shallow clone cannot run it)`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
lines.push(`agreement: ${open.length} open change(s), ${started} with completed tasks`);
|
|
249
|
+
lines.push(staleChecked === 0 ? "stale: vacuous (no open agreed change modifies, removes or renames a requirement)" : `stale: ${staleChecked} targeted living spec(s) checked against their agreed commits`);
|
|
250
|
+
// 4. archived changes
|
|
251
|
+
const archived = await dirs(join(repo, STORE_DIR, "changes", "archive"));
|
|
252
|
+
let baselines = 0;
|
|
253
|
+
for (const name of archived) {
|
|
254
|
+
const dir = `${STORE_DIR}/changes/archive/${name}`;
|
|
255
|
+
if (await exists(join(repo, dir, "baseline-evidence.md"))) {
|
|
256
|
+
baselines++;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const change = name.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
260
|
+
if (!(await exists(join(repo, dir, "verdict.md"))))
|
|
261
|
+
failures.push(`${dir}: archived without verdict.md`);
|
|
262
|
+
const record = records.get(change);
|
|
263
|
+
if (!record) {
|
|
264
|
+
failures.push(`${dir}: archived without an agreement record (${AGREEMENTS_DIR}/${change}.yaml)`);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (record.verdict?.result !== "PASS")
|
|
268
|
+
failures.push(`${record.file}: change is archived but verdict.result is ${JSON.stringify(record.verdict?.result)}, not PASS`);
|
|
269
|
+
const vr = record.verdict?.record;
|
|
270
|
+
if (typeof vr !== "string" || !(await exists(join(repo, vr))))
|
|
271
|
+
failures.push(`${record.file}: verdict.record ${JSON.stringify(vr)} does not name an existing file`);
|
|
272
|
+
}
|
|
273
|
+
lines.push(`verdicts: ${archived.length} archived change(s), ${baselines} baseline(s), ${archived.length - baselines} needing a PASS verdict`);
|
|
274
|
+
const population = recordFiles.length + open.length + archived.length;
|
|
275
|
+
const body = lines.map((l) => ` ${l}`).join("\n") + "\n";
|
|
276
|
+
if (failures.length > 0) {
|
|
277
|
+
return { exitCode: 1, stdout: body + failures.map((f) => `[FAIL] ${f}`).join("\n") + "\n" };
|
|
278
|
+
}
|
|
279
|
+
if (population === 0)
|
|
280
|
+
return { exitCode: 0, stdout: body + `[N/A] the behaviour store has no changes and no agreement records yet\n` };
|
|
281
|
+
return { exitCode: 0, stdout: body + "[PASS] behaviour store: records, agreement, stale and verdict checks hold\n" };
|
|
282
|
+
}
|
|
283
|
+
async function runOpenspec(repo, args) {
|
|
284
|
+
try {
|
|
285
|
+
const { stdout } = await execFileAsync("openspec", args, { cwd: repo, env: { ...process.env, ...OPENSPEC_ENV }, maxBuffer: 16 * 1024 * 1024 });
|
|
286
|
+
return { code: 0, json: JSON.parse(stdout), raw: stdout };
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
const e = err;
|
|
290
|
+
let json;
|
|
291
|
+
try {
|
|
292
|
+
json = JSON.parse(e.stdout ?? "");
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
json = undefined;
|
|
296
|
+
}
|
|
297
|
+
return { code: typeof e.code === "number" ? e.code : 127, ...(json ? { json } : {}), raw: (e.stdout ?? "") + (e.stderr ?? "") };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** OpenSpec's own strict validation of the store: specs and open changes, and archived changes' tasks. */
|
|
301
|
+
export async function checkBehaviourStoreOpenspec(repo) {
|
|
302
|
+
if (!(await exists(join(repo, STORE_DIR)))) {
|
|
303
|
+
return { exitCode: 0, stdout: `[N/A] no behaviour store (${STORE_DIR}/) in this repository\n` };
|
|
304
|
+
}
|
|
305
|
+
let version = "";
|
|
306
|
+
try {
|
|
307
|
+
version = (await execFileAsync("openspec", ["--version"], { env: { ...process.env, ...OPENSPEC_ENV } })).stdout.trim();
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return {
|
|
311
|
+
exitCode: 1,
|
|
312
|
+
stdout: `[FAIL] the OpenSpec CLI is not on PATH; this repository has a behaviour store, so its validation cannot run (install @fission-ai/openspec@${OPENSPEC_VERSION}; the Method Validation module does)\n`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if (version !== OPENSPEC_VERSION) {
|
|
316
|
+
return { exitCode: 1, stdout: `[FAIL] OpenSpec ${version} is installed; the method depends on exactly ${OPENSPEC_VERSION}\n` };
|
|
317
|
+
}
|
|
318
|
+
const lines = [];
|
|
319
|
+
const failures = [];
|
|
320
|
+
let measured = 0;
|
|
321
|
+
for (const [label, args] of [
|
|
322
|
+
["specs and open changes (--all --strict)", ["validate", "--all", "--strict", "--json", "--no-interactive"]],
|
|
323
|
+
["archived changes' tasks (--archived)", ["validate", "--archived", "--json", "--no-interactive"]],
|
|
324
|
+
]) {
|
|
325
|
+
const r = await runOpenspec(repo, [...args]);
|
|
326
|
+
const t = r.json?.summary?.totals;
|
|
327
|
+
if (!t) {
|
|
328
|
+
failures.push(`${label}: OpenSpec returned no report (exit ${r.code}): ${r.raw.trim().split("\n").slice(-3).join(" | ")}`);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
measured += t.items ?? 0;
|
|
332
|
+
lines.push(`${label}: ${t.items ?? 0} item(s), ${t.passed ?? 0} passed, ${t.failed ?? 0} failed`);
|
|
333
|
+
for (const item of r.json?.items ?? []) {
|
|
334
|
+
if (item.valid)
|
|
335
|
+
continue;
|
|
336
|
+
for (const i of item.issues ?? [])
|
|
337
|
+
failures.push(`${label}: ${item.id}${i.path ? ` (${i.path})` : ""}: ${i.level} ${i.message}`);
|
|
338
|
+
if ((item.issues ?? []).length === 0)
|
|
339
|
+
failures.push(`${label}: ${item.id}: invalid`);
|
|
340
|
+
}
|
|
341
|
+
if (r.code !== 0 && (t.failed ?? 0) === 0)
|
|
342
|
+
failures.push(`${label}: OpenSpec exited ${r.code} with no failed item reported`);
|
|
343
|
+
}
|
|
344
|
+
const body = [` OpenSpec ${version}`, ...lines.map((l) => ` ${l}`)].join("\n") + "\n";
|
|
345
|
+
if (failures.length > 0)
|
|
346
|
+
return { exitCode: 1, stdout: body + failures.map((f) => `[FAIL] ${f}`).join("\n") + "\n" };
|
|
347
|
+
if (measured === 0)
|
|
348
|
+
return { exitCode: 0, stdout: body + "[N/A] OpenSpec found no specs and no changes to validate\n" };
|
|
349
|
+
return { exitCode: 0, stdout: body + "[PASS] OpenSpec strict validation holds\n" };
|
|
350
|
+
}
|
|
351
|
+
//# sourceMappingURL=behaviour-store.js.map
|
|
@@ -1,5 +1,20 @@
|
|
|
1
|
+
import { checkBehaviourStore, checkBehaviourStoreOpenspec } from "./behaviour-store.js";
|
|
1
2
|
import { checkBundleCoherence } from "./bundle-coherence.js";
|
|
2
3
|
import { checkYamlParse } from "./yaml-parse.js";
|
|
4
|
+
// The behaviour store (M-ADR 0221-0223) can sit in an SDD of any profile, so every profile carries
|
|
5
|
+
// its two checks; without a store both report [N/A], an empty population, not a pass.
|
|
6
|
+
const BEHAVIOUR_STORE_VALIDATORS = [
|
|
7
|
+
{
|
|
8
|
+
name: "behaviour-store",
|
|
9
|
+
source: { kind: "native", run: async (ctx) => checkBehaviourStore(ctx.targetRepo) },
|
|
10
|
+
build: () => ({ status: "run", args: [] }),
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: "behaviour-store-openspec",
|
|
14
|
+
source: { kind: "native", run: async (ctx) => checkBehaviourStoreOpenspec(ctx.targetRepo) },
|
|
15
|
+
build: () => ({ status: "run", args: [] }),
|
|
16
|
+
},
|
|
17
|
+
];
|
|
3
18
|
// Validators that accept --app-root / --repo-root flags work fine from the
|
|
4
19
|
// bundled location because they override their internal ROOT_DIR via the
|
|
5
20
|
// flag. Validators without that flag (adr-structure, runway-classification,
|
|
@@ -74,6 +89,7 @@ const APP_VALIDATORS = [
|
|
|
74
89
|
},
|
|
75
90
|
build: () => ({ status: "run", args: [] }),
|
|
76
91
|
},
|
|
92
|
+
...BEHAVIOUR_STORE_VALIDATORS,
|
|
77
93
|
// Note: validate-runway-classification is intentionally NOT in the app set.
|
|
78
94
|
// It requires platform-level files (docs/platform/feature-tree.md,
|
|
79
95
|
// architecture-runway-index.md) that app repos don't own. Per ADR 0089,
|
|
@@ -126,6 +142,7 @@ const INTEGRATION_VALIDATORS = [
|
|
|
126
142
|
},
|
|
127
143
|
build: () => ({ status: "run", args: [] }),
|
|
128
144
|
},
|
|
145
|
+
...BEHAVIOUR_STORE_VALIDATORS,
|
|
129
146
|
];
|
|
130
147
|
const PLATFORM_VALIDATORS = [
|
|
131
148
|
{
|
|
@@ -208,6 +225,7 @@ const PLATFORM_VALIDATORS = [
|
|
|
208
225
|
},
|
|
209
226
|
build: () => ({ status: "run", args: [] }),
|
|
210
227
|
},
|
|
228
|
+
...BEHAVIOUR_STORE_VALIDATORS,
|
|
211
229
|
// Optional: the upstream validate-docs.sh dispatcher covers the 6
|
|
212
230
|
// conditional validators (backward-compatibility, deprecation-runway,
|
|
213
231
|
// migration-completeness, ddd-artifacts, glossary-alignment,
|
|
@@ -22,6 +22,8 @@ const TITLES = {
|
|
|
22
22
|
"yaml-parse": "Validate that tracked YAML parses",
|
|
23
23
|
"gate-readiness": "Validate gate readiness",
|
|
24
24
|
"bundle-coherence": "Report bundle coherence",
|
|
25
|
+
"behaviour-store": "Validate the behaviour store: agreement records, the pre-apply gate, stale requirements, verdicts",
|
|
26
|
+
"behaviour-store-openspec": "Validate the behaviour store with OpenSpec (strict)",
|
|
25
27
|
// CI has no platform checkout, so this step runs the app-only checks; the met gate needs the platform beside it.
|
|
26
28
|
"app-dependencies": "Validate app dependencies (app-only checks; no platform checkout in CI)",
|
|
27
29
|
};
|
package/dist/verify-version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdd-method/verify-cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"sddCliVersion": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"sddCliVersion": "2.0.0",
|
|
5
5
|
"description": "Verify an SDD repository without changing it — validate, list, and read-only catalogue and pack checks. Ships only the code those verbs reach; it cannot write to the repository it is pointed at. Authoring and maintenance are in @sdd-method/sdd-cli.",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
9
|
"sdd-verify": "dist/verify-bin.js"
|
|
@@ -23,8 +23,6 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|
|
25
25
|
"scripts",
|
|
26
|
-
"README.md"
|
|
27
|
-
"LICENSE",
|
|
28
|
-
"NOTICE"
|
|
26
|
+
"README.md"
|
|
29
27
|
]
|
|
30
28
|
}
|
|
@@ -11,23 +11,23 @@
|
|
|
11
11
|
#
|
|
12
12
|
# Checks (for each entry in app-platform-dependencies.yaml), split by whether they
|
|
13
13
|
# need a platform surface at all — LOCAL checks run wherever the app repo is:
|
|
14
|
-
# 1. (local) required_stability is a valid value (provisional |
|
|
14
|
+
# 1. (local) required_stability is a valid value (provisional | stable | unknown)
|
|
15
15
|
# 2a. (local) service_id is declared and non-empty
|
|
16
16
|
# 2b. (platform) service_id resolves against the platform source of truth:
|
|
17
17
|
# catalogues/services.csv (exact id match) when the catalogue exists,
|
|
18
18
|
# else docs/platform/service-architecture.md (word-anchored match)
|
|
19
|
-
# 3a. (local) a `
|
|
19
|
+
# 3a. (local) a `stable` dependency declares a contract_path at all
|
|
20
20
|
# 3b. (platform) that contract_path resolves to a file under the platform root
|
|
21
21
|
#
|
|
22
22
|
# Outputs a dependency status table.
|
|
23
|
-
# Used as the Stage
|
|
23
|
+
# Used as the Stage 9 agreement gate in the App Exploration Workflow.
|
|
24
24
|
#
|
|
25
25
|
# THE STABILITY VOCABULARY, AND WHY IT IS WHAT IT IS
|
|
26
26
|
# `required_stability` states the CONSUMING app's expectation of contract
|
|
27
27
|
# stability. Three values express that:
|
|
28
28
|
#
|
|
29
29
|
# provisional the contract may still change; the app tolerates it
|
|
30
|
-
#
|
|
30
|
+
# stable the contract is fixed; a contract_path must exist (check 3)
|
|
31
31
|
# unknown the expectation is not determined
|
|
32
32
|
#
|
|
33
33
|
# `unknown` is accepted because the canonical schema's
|
|
@@ -157,8 +157,8 @@ APP_DEPS_FILE="$APP_ROOT/app-platform-dependencies.yaml"
|
|
|
157
157
|
#
|
|
158
158
|
# This gate read only the forward file, so an entire equal-status source went
|
|
159
159
|
# ungated. Measured in one reverse estate: 15 evidence files declaring 65
|
|
160
|
-
# further dependencies, 38 of them `locked` — precisely the
|
|
161
|
-
# a Stage
|
|
160
|
+
# further dependencies, 38 of them `locked` — precisely the population
|
|
161
|
+
# a Stage 9 agreement gate exists to check — none of which this gate could see.
|
|
162
162
|
#
|
|
163
163
|
# A source that the catalogue reads and the gate does not is a coverage gap by
|
|
164
164
|
# construction, so the gate now reads both.
|
|
@@ -180,7 +180,7 @@ PLATFORM_SERVICES_CSV="$PLATFORM_ROOT/catalogues/services.csv"
|
|
|
180
180
|
# offer (instances app-dependency-stability-vocabulary-forward and -reverse).
|
|
181
181
|
VALID_STABILITY_VALUES=(
|
|
182
182
|
"provisional" # contract may still change; the app tolerates it
|
|
183
|
-
"
|
|
183
|
+
"stable" # contract is fixed; check 3 requires a resolvable contract_path
|
|
184
184
|
"unknown" # expectation not determined; no contract_path required
|
|
185
185
|
)
|
|
186
186
|
|
|
@@ -353,6 +353,13 @@ validate_dependency() {
|
|
|
353
353
|
service_id="$(yq e ".platform_dependencies[$index].service_id // \"\"" "$source_file")"
|
|
354
354
|
contract_type="$(yq e ".platform_dependencies[$index].contract_type // \"\"" "$source_file")"
|
|
355
355
|
required_stability="$(yq e ".platform_dependencies[$index].required_stability // \"\"" "$source_file")"
|
|
356
|
+
# vocabulary-alias: locked -> stable
|
|
357
|
+
# The fixed-contract stability class was `locked` (M-ADR 0222 §4.1; docs/method/foundations/vocabulary-renames.yaml).
|
|
358
|
+
# Read as `stable` for one major release and reported; from sdd-cli 3.0.0 it is invalid.
|
|
359
|
+
if [[ "$required_stability" == "locked" ]]; then
|
|
360
|
+
log_warn "$source_file: platform_dependencies[$index] uses the former required_stability 'locked', read as 'stable' (M-ADR 0222 §4.1). Run 'sdd-cli migrate vocabulary'; from sdd-cli 3.0.0 the former value is invalid."
|
|
361
|
+
required_stability="stable"
|
|
362
|
+
fi
|
|
356
363
|
contract_path="$(yq e ".platform_dependencies[$index].contract_path // \"\"" "$source_file")"
|
|
357
364
|
|
|
358
365
|
local dep_label="${service_id:-dep[$index]}"
|
|
@@ -403,7 +410,7 @@ validate_dependency() {
|
|
|
403
410
|
done
|
|
404
411
|
|
|
405
412
|
if [[ "$stability_deprecated" -eq 1 ]]; then
|
|
406
|
-
log_fail "$dep_label: required_stability '$required_stability' is not a stability value (M-ADR 0204 §4.4). Declare target_locus: '$required_stability' on the entry and give required_stability a real value (${VALID_STABILITY_VALUES[*]}) — a third-party contract can be
|
|
413
|
+
log_fail "$dep_label: required_stability '$required_stability' is not a stability value (M-ADR 0204 §4.4). Declare target_locus: '$required_stability' on the entry and give required_stability a real value (${VALID_STABILITY_VALUES[*]}) — a third-party contract can be stable."
|
|
407
414
|
((dep_errors++))
|
|
408
415
|
else
|
|
409
416
|
log_fail "$dep_label: invalid required_stability '$required_stability' (allowed: ${VALID_STABILITY_VALUES[*]})"
|
|
@@ -490,26 +497,26 @@ validate_dependency() {
|
|
|
490
497
|
((WARNINGS++))
|
|
491
498
|
fi
|
|
492
499
|
|
|
493
|
-
# Check 3: a
|
|
494
|
-
# resolves to a file (3b, needs the platform). Same split, same reason: "
|
|
500
|
+
# Check 3: a stable dependency declares a contract_path (3a, local) and that path
|
|
501
|
+
# resolves to a file (3b, needs the platform). Same split, same reason: "stable
|
|
495
502
|
# with no path at all" is an app-repo defect that needs nothing external to catch.
|
|
496
|
-
if [[ "$required_stability" == "
|
|
503
|
+
if [[ "$required_stability" == "stable" ]]; then
|
|
497
504
|
if [[ -z "$contract_path" || "$contract_path" == "null" ]]; then
|
|
498
|
-
log_fail "$dep_label: required_stability is '
|
|
505
|
+
log_fail "$dep_label: required_stability is 'stable' but contract_path is empty"
|
|
499
506
|
((dep_errors++))
|
|
500
507
|
elif [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
|
|
501
508
|
((UNRESOLVED_CONTRACT_PATHS++))
|
|
502
509
|
else
|
|
503
510
|
local full_contract_path="$PLATFORM_ROOT/$contract_path"
|
|
504
511
|
if [[ ! -f "$full_contract_path" ]]; then
|
|
505
|
-
log_fail "$dep_label:
|
|
512
|
+
log_fail "$dep_label: stable contract_path '$contract_path' does not exist in platform repo"
|
|
506
513
|
((dep_errors++))
|
|
507
514
|
fi
|
|
508
515
|
fi
|
|
509
516
|
elif [[ "$required_stability" == "provisional" || "$required_stability" == "unknown" ]]; then
|
|
510
517
|
# Neither requires a contract_path. If one is declared it is still worth
|
|
511
518
|
# reporting when it does not resolve — silently accepting a path that
|
|
512
|
-
# points at nothing is how a
|
|
519
|
+
# points at nothing is how a stable dependency later inherits a dead path.
|
|
513
520
|
if [[ -n "$contract_path" && "$contract_path" != "null" && "$PLATFORM_AVAILABLE" -eq 1 ]]; then
|
|
514
521
|
local full_contract_path="$PLATFORM_ROOT/$contract_path"
|
|
515
522
|
if [[ ! -f "$full_contract_path" ]]; then
|
|
@@ -552,21 +559,22 @@ print_status_table() {
|
|
|
552
559
|
local service_id required_stability contract_type contract_path dep_status
|
|
553
560
|
service_id="$(yq e ".platform_dependencies[$i].service_id // \"(missing)\"" "$source_file")"
|
|
554
561
|
required_stability="$(yq e ".platform_dependencies[$i].required_stability // \"(missing)\"" "$source_file")"
|
|
562
|
+
if [[ "$required_stability" == "locked" ]]; then required_stability="stable"; fi # the former name, reported above
|
|
555
563
|
contract_type="$(yq e ".platform_dependencies[$i].contract_type // \"(missing)\"" "$source_file")"
|
|
556
564
|
contract_path="$(yq e ".platform_dependencies[$i].contract_path // \"\"" "$source_file")"
|
|
557
565
|
|
|
558
566
|
# Determine display status
|
|
559
|
-
if [[ "$required_stability" == "
|
|
567
|
+
if [[ "$required_stability" == "stable" ]]; then
|
|
560
568
|
if [[ -z "$contract_path" || "$contract_path" == "null" ]]; then
|
|
561
|
-
dep_status="${RED}
|
|
569
|
+
dep_status="${RED}STABLE UNMET${NC}"
|
|
562
570
|
elif [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
|
|
563
571
|
# A declared path that was never resolved is not "OK" and not "UNMET".
|
|
564
572
|
# Reporting either would be a claim the run cannot support.
|
|
565
|
-
dep_status="${YELLOW}
|
|
573
|
+
dep_status="${YELLOW}STABLE UNRESOLVED${NC}"
|
|
566
574
|
elif [[ -f "$PLATFORM_ROOT/$contract_path" ]]; then
|
|
567
|
-
dep_status="${GREEN}
|
|
575
|
+
dep_status="${GREEN}STABLE OK${NC}"
|
|
568
576
|
else
|
|
569
|
-
dep_status="${RED}
|
|
577
|
+
dep_status="${RED}STABLE UNMET${NC}"
|
|
570
578
|
fi
|
|
571
579
|
elif [[ "$required_stability" == "provisional" ]]; then
|
|
572
580
|
dep_status="${YELLOW}PROVISIONAL${NC}"
|
|
@@ -637,7 +645,7 @@ main() {
|
|
|
637
645
|
fi
|
|
638
646
|
|
|
639
647
|
if [[ "$FAILURES" -gt 0 ]]; then
|
|
640
|
-
log_fail "$FAILURES dependency check(s) failed. Stage
|
|
648
|
+
log_fail "$FAILURES dependency check(s) failed. Stage 9 agreement gate NOT met."
|
|
641
649
|
exit 1
|
|
642
650
|
fi
|
|
643
651
|
|
|
@@ -648,7 +656,7 @@ main() {
|
|
|
648
656
|
# while never executing (M-ADR 0200 §4.3).
|
|
649
657
|
if [[ "$PLATFORM_AVAILABLE" -eq 0 ]]; then
|
|
650
658
|
log_warn "Ran check 1 (stability vocabulary) and the local halves of checks 2-3 over $TOTAL_DEPS dependency(ies): $FAILURES failure(s), $WARNINGS warning(s)."
|
|
651
|
-
log_warn "NOT RUN, for want of --platform-root: $UNRESOLVED_SERVICE_IDS service_id resolution(s) and $UNRESOLVED_CONTRACT_PATHS
|
|
659
|
+
log_warn "NOT RUN, for want of --platform-root: $UNRESOLVED_SERVICE_IDS service_id resolution(s) and $UNRESOLVED_CONTRACT_PATHS stable contract_path resolution(s)."
|
|
652
660
|
log_info "This is a partial run, not a met gate — pass --platform-root to complete it."
|
|
653
661
|
exit 0
|
|
654
662
|
fi
|
|
@@ -656,7 +664,7 @@ main() {
|
|
|
656
664
|
if [[ "$WARNINGS" -gt 0 ]]; then
|
|
657
665
|
log_warn "All required checks passed with $WARNINGS warning(s)."
|
|
658
666
|
else
|
|
659
|
-
log_pass "All dependency checks passed. Stage
|
|
667
|
+
log_pass "All dependency checks passed. Stage 9 agreement gate met."
|
|
660
668
|
fi
|
|
661
669
|
exit 0
|
|
662
670
|
}
|
|
@@ -341,7 +341,7 @@ check_grounding_doc() {
|
|
|
341
341
|
|
|
342
342
|
local grounding="$REPO_ROOT/docs/design/integration/feature-grounding.md"
|
|
343
343
|
if [[ ! -f "$grounding" ]]; then
|
|
344
|
-
log_warn "docs/design/integration/feature-grounding.md not found — create before
|
|
344
|
+
log_warn "docs/design/integration/feature-grounding.md not found — create before agreement"
|
|
345
345
|
return
|
|
346
346
|
fi
|
|
347
347
|
|
package/LICENSE
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
Apache License
|
|
2
|
-
Version 2.0, January 2004
|
|
3
|
-
http://www.apache.org/licenses/
|
|
4
|
-
|
|
5
|
-
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
-
|
|
7
|
-
1. Definitions.
|
|
8
|
-
|
|
9
|
-
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
-
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
-
|
|
12
|
-
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
-
the copyright owner that is granting the License.
|
|
14
|
-
|
|
15
|
-
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
-
other entities that control, are controlled by, or are under common
|
|
17
|
-
control with that entity. For the purposes of this definition,
|
|
18
|
-
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
-
direction or management of such entity, whether by contract or
|
|
20
|
-
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
-
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
-
|
|
23
|
-
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
-
exercising permissions granted by this License.
|
|
25
|
-
|
|
26
|
-
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
-
including but not limited to software source code, documentation
|
|
28
|
-
source, and configuration files.
|
|
29
|
-
|
|
30
|
-
"Object" form shall mean any form resulting from mechanical
|
|
31
|
-
transformation or translation of a Source form, including but
|
|
32
|
-
not limited to compiled object code, generated documentation,
|
|
33
|
-
and conversions to other media types.
|
|
34
|
-
|
|
35
|
-
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
-
Object form, made available under the License, as indicated by a
|
|
37
|
-
copyright notice that is included in or attached to the work
|
|
38
|
-
(an example is provided in the Appendix below).
|
|
39
|
-
|
|
40
|
-
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
-
form, that is based on (or derived from) the Work and for which the
|
|
42
|
-
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
-
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
-
of this License, Derivative Works shall not include works that remain
|
|
45
|
-
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
-
the Work and Derivative Works thereof.
|
|
47
|
-
|
|
48
|
-
"Contribution" shall mean any work of authorship, including
|
|
49
|
-
the original version of the Work and any modifications or additions
|
|
50
|
-
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
-
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
-
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
-
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
-
means any form of electronic, verbal, or written communication sent
|
|
55
|
-
to the Licensor or its representatives, including but not limited to
|
|
56
|
-
communication on electronic mailing lists, source code control systems,
|
|
57
|
-
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
-
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
-
excluding communication that is conspicuously marked or otherwise
|
|
60
|
-
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
-
|
|
62
|
-
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
-
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
-
subsequently incorporated within the Work.
|
|
65
|
-
|
|
66
|
-
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
-
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
-
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
-
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
-
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
-
Work and such Derivative Works in Source or Object form.
|
|
72
|
-
|
|
73
|
-
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
-
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
-
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
-
(except as stated in this section) patent license to make, have made,
|
|
77
|
-
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
-
where such license applies only to those patent claims licensable
|
|
79
|
-
by such Contributor that are necessarily infringed by their
|
|
80
|
-
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
-
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
-
institute patent litigation against any entity (including a
|
|
83
|
-
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
-
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
-
or contributory patent infringement, then any patent licenses
|
|
86
|
-
granted to You under this License for that Work shall terminate
|
|
87
|
-
as of the date such litigation is filed.
|
|
88
|
-
|
|
89
|
-
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
-
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
-
modifications, and in Source or Object form, provided that You
|
|
92
|
-
meet the following conditions:
|
|
93
|
-
|
|
94
|
-
(a) You must give any other recipients of the Work or
|
|
95
|
-
Derivative Works a copy of this License; and
|
|
96
|
-
|
|
97
|
-
(b) You must cause any modified files to carry prominent notices
|
|
98
|
-
stating that You changed the files; and
|
|
99
|
-
|
|
100
|
-
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
-
that You distribute, all copyright, patent, trademark, and
|
|
102
|
-
attribution notices from the Source form of the Work,
|
|
103
|
-
excluding those notices that do not pertain to any part of
|
|
104
|
-
the Derivative Works; and
|
|
105
|
-
|
|
106
|
-
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
-
distribution, then any Derivative Works that You distribute must
|
|
108
|
-
include a readable copy of the attribution notices contained
|
|
109
|
-
within such NOTICE file, excluding those notices that do not
|
|
110
|
-
pertain to any part of the Derivative Works, in at least one
|
|
111
|
-
of the following places: within a NOTICE text file distributed
|
|
112
|
-
as part of the Derivative Works; within the Source form or
|
|
113
|
-
documentation, if provided along with the Derivative Works; or,
|
|
114
|
-
within a display generated by the Derivative Works, if and
|
|
115
|
-
wherever such third-party notices normally appear. The contents
|
|
116
|
-
of the NOTICE file are for informational purposes only and
|
|
117
|
-
do not modify the License. You may add Your own attribution
|
|
118
|
-
notices within Derivative Works that You distribute, alongside
|
|
119
|
-
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
-
that such additional attribution notices cannot be construed
|
|
121
|
-
as modifying the License.
|
|
122
|
-
|
|
123
|
-
You may add Your own copyright statement to Your modifications and
|
|
124
|
-
may provide additional or different license terms and conditions
|
|
125
|
-
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
-
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
-
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
-
the conditions stated in this License.
|
|
129
|
-
|
|
130
|
-
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
-
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
-
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
-
this License, without any additional terms or conditions.
|
|
134
|
-
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
-
the terms of any separate license agreement you may have executed
|
|
136
|
-
with Licensor regarding such Contributions.
|
|
137
|
-
|
|
138
|
-
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
-
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
-
except as required for reasonable and customary use in describing the
|
|
141
|
-
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
-
|
|
143
|
-
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
-
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
-
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
-
implied, including, without limitation, any warranties or conditions
|
|
148
|
-
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
-
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
-
appropriateness of using or redistributing the Work and assume any
|
|
151
|
-
risks associated with Your exercise of permissions under this License.
|
|
152
|
-
|
|
153
|
-
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
-
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
-
unless required by applicable law (such as deliberate and grossly
|
|
156
|
-
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
-
liable to You for damages, including any direct, indirect, special,
|
|
158
|
-
incidental, or consequential damages of any character arising as a
|
|
159
|
-
result of this License or out of the use or inability to use the
|
|
160
|
-
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
-
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
-
other commercial damages or losses), even if such Contributor
|
|
163
|
-
has been advised of the possibility of such damages.
|
|
164
|
-
|
|
165
|
-
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
-
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
-
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
-
or other liability obligations and/or rights consistent with this
|
|
169
|
-
License. However, in accepting such obligations, You may act only
|
|
170
|
-
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
-
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
-
defend, and hold each Contributor harmless for any liability
|
|
173
|
-
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
-
of your accepting any such warranty or additional liability.
|
|
175
|
-
|
|
176
|
-
END OF TERMS AND CONDITIONS
|
|
177
|
-
|
|
178
|
-
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
-
|
|
180
|
-
To apply the Apache License to your work, attach the following
|
|
181
|
-
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
-
replaced with your own identifying information. (Don't include
|
|
183
|
-
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
-
comment syntax for the file format. We also recommend that a
|
|
185
|
-
file or class name and description of purpose be included on the
|
|
186
|
-
same "printed page" as the copyright notice for easier
|
|
187
|
-
identification within third-party archives.
|
|
188
|
-
|
|
189
|
-
Copyright [yyyy] [name of copyright owner]
|
|
190
|
-
|
|
191
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
-
you may not use this file except in compliance with the License.
|
|
193
|
-
You may obtain a copy of the License at
|
|
194
|
-
|
|
195
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
-
|
|
197
|
-
Unless required by applicable law or agreed to in writing, software
|
|
198
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
-
See the License for the specific language governing permissions and
|
|
201
|
-
limitations under the License.
|
package/NOTICE
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
sdd-cli
|
|
2
|
-
Copyright 2026 Tom Foster
|
|
3
|
-
|
|
4
|
-
This product includes software developed as part of the SDD method
|
|
5
|
-
distribution.
|
|
6
|
-
|
|
7
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
-
you may not use this file except in compliance with the License.
|
|
9
|
-
You may obtain a copy of the License at
|
|
10
|
-
|
|
11
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
-
|
|
13
|
-
Unless required by applicable law or agreed to in writing, software
|
|
14
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
-
See the License for the specific language governing permissions and
|
|
17
|
-
limitations under the License.
|