@clossys/launcher 0.3.0 → 0.4.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 +1328 -60
- package/contracts/conversation-contract.md +2 -1
- package/contracts/product-ci-workflow.yml +74 -0
- package/contracts/repository-inventory.json +53 -0
- package/dist/admission-fixture.d.ts +168 -0
- package/dist/admission-fixture.d.ts.map +1 -0
- package/dist/admission-fixture.js +453 -0
- package/dist/admission-fixture.js.map +1 -0
- package/dist/admission.d.ts +124 -0
- package/dist/admission.d.ts.map +1 -0
- package/dist/admission.js +799 -0
- package/dist/admission.js.map +1 -0
- package/dist/agents-guide.d.ts +9 -0
- package/dist/agents-guide.d.ts.map +1 -0
- package/dist/agents-guide.js +26 -0
- package/dist/agents-guide.js.map +1 -0
- package/dist/apply-command-options.check.d.ts +12 -0
- package/dist/apply-command-options.check.d.ts.map +1 -0
- package/dist/apply-command-options.check.js +20 -0
- package/dist/apply-command-options.check.js.map +1 -0
- package/dist/apply-plan-cli.d.ts +39 -1
- package/dist/apply-plan-cli.d.ts.map +1 -1
- package/dist/apply-plan-cli.js +432 -15
- package/dist/apply-plan-cli.js.map +1 -1
- package/dist/apply-plan.d.ts +46 -59
- package/dist/apply-plan.d.ts.map +1 -1
- package/dist/apply-plan.js +112 -97
- package/dist/apply-plan.js.map +1 -1
- package/dist/apply-step-fixture.d.ts +87 -0
- package/dist/apply-step-fixture.d.ts.map +1 -0
- package/dist/apply-step-fixture.js +199 -0
- package/dist/apply-step-fixture.js.map +1 -0
- package/dist/apply-store.d.ts +93 -0
- package/dist/apply-store.d.ts.map +1 -0
- package/dist/apply-store.js +625 -0
- package/dist/apply-store.js.map +1 -0
- package/dist/approval-sheet.d.ts +21 -0
- package/dist/approval-sheet.d.ts.map +1 -0
- package/dist/approval-sheet.js +157 -0
- package/dist/approval-sheet.js.map +1 -0
- package/dist/body-command.d.ts +42 -0
- package/dist/body-command.d.ts.map +1 -0
- package/dist/body-command.js +143 -0
- package/dist/body-command.js.map +1 -0
- package/dist/change-set-contract.d.ts +381 -0
- package/dist/change-set-contract.d.ts.map +1 -0
- package/dist/change-set-contract.js +738 -0
- package/dist/change-set-contract.js.map +1 -0
- package/dist/change-set-digest.d.ts +28 -0
- package/dist/change-set-digest.d.ts.map +1 -0
- package/dist/change-set-digest.js +65 -0
- package/dist/change-set-digest.js.map +1 -0
- package/dist/check-cli.d.ts.map +1 -1
- package/dist/check-cli.js +14 -3
- package/dist/check-cli.js.map +1 -1
- package/dist/cli.d.ts +17 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +84 -23
- package/dist/cli.js.map +1 -1
- package/dist/core.d.ts +79 -22
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +843 -268
- package/dist/core.js.map +1 -1
- package/dist/dry-materialize.d.ts +63 -0
- package/dist/dry-materialize.d.ts.map +1 -0
- package/dist/dry-materialize.js +330 -0
- package/dist/dry-materialize.js.map +1 -0
- package/dist/generated/contract-schema.generated.d.ts +97 -0
- package/dist/generated/contract-schema.generated.d.ts.map +1 -0
- package/dist/generated/contract-schema.generated.js +496 -0
- package/dist/generated/contract-schema.generated.js.map +1 -0
- package/dist/generated/package-scope.generated.d.ts +6 -0
- package/dist/generated/package-scope.generated.d.ts.map +1 -0
- package/dist/generated/package-scope.generated.js +10 -0
- package/dist/generated/package-scope.generated.js.map +1 -0
- package/dist/generated/plan-contracts.generated.d.ts +3 -0
- package/dist/generated/plan-contracts.generated.d.ts.map +1 -0
- package/dist/generated/plan-contracts.generated.js +2840 -0
- package/dist/generated/plan-contracts.generated.js.map +1 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +11 -0
- package/dist/host.js.map +1 -1
- package/dist/identity.d.ts +15 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +48 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +34 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/index.js.map +1 -1
- package/dist/inventory-adoption.d.ts +24 -5
- package/dist/inventory-adoption.d.ts.map +1 -1
- package/dist/inventory-adoption.js +70 -25
- package/dist/inventory-adoption.js.map +1 -1
- package/dist/inventory-choice.d.ts +40 -0
- package/dist/inventory-choice.d.ts.map +1 -0
- package/dist/inventory-choice.js +156 -0
- package/dist/inventory-choice.js.map +1 -0
- package/dist/inventory-contract.d.ts +89 -0
- package/dist/inventory-contract.d.ts.map +1 -0
- package/dist/inventory-contract.js +121 -0
- package/dist/inventory-contract.js.map +1 -0
- package/dist/key-editor.d.ts +30 -0
- package/dist/key-editor.d.ts.map +1 -0
- package/dist/key-editor.js +445 -0
- package/dist/key-editor.js.map +1 -0
- package/dist/ledger-contract.d.ts +187 -0
- package/dist/ledger-contract.d.ts.map +1 -0
- package/dist/ledger-contract.js +532 -0
- package/dist/ledger-contract.js.map +1 -0
- package/dist/ledger-trust.d.ts +90 -0
- package/dist/ledger-trust.d.ts.map +1 -0
- package/dist/ledger-trust.js +198 -0
- package/dist/ledger-trust.js.map +1 -0
- package/dist/lockfile-invariants.d.ts +48 -0
- package/dist/lockfile-invariants.d.ts.map +1 -0
- package/dist/lockfile-invariants.js +375 -0
- package/dist/lockfile-invariants.js.map +1 -0
- package/dist/lockfile-readers.d.ts +72 -0
- package/dist/lockfile-readers.d.ts.map +1 -0
- package/dist/lockfile-readers.js +713 -0
- package/dist/lockfile-readers.js.map +1 -0
- package/dist/lockfile-regen.d.ts +106 -0
- package/dist/lockfile-regen.d.ts.map +1 -0
- package/dist/lockfile-regen.js +760 -0
- package/dist/lockfile-regen.js.map +1 -0
- package/dist/lockfile-tool-env.d.ts +29 -0
- package/dist/lockfile-tool-env.d.ts.map +1 -0
- package/dist/lockfile-tool-env.js +111 -0
- package/dist/lockfile-tool-env.js.map +1 -0
- package/dist/materialize.d.ts +113 -0
- package/dist/materialize.d.ts.map +1 -0
- package/dist/materialize.js +840 -0
- package/dist/materialize.js.map +1 -0
- package/dist/observe-repository.d.ts +90 -0
- package/dist/observe-repository.d.ts.map +1 -0
- package/dist/observe-repository.js +1367 -0
- package/dist/observe-repository.js.map +1 -0
- package/dist/plan-bundle-setup-fixture.d.ts +68 -0
- package/dist/plan-bundle-setup-fixture.d.ts.map +1 -0
- package/dist/plan-bundle-setup-fixture.js +167 -0
- package/dist/plan-bundle-setup-fixture.js.map +1 -0
- package/dist/plan-bundle.d.ts +250 -0
- package/dist/plan-bundle.d.ts.map +1 -0
- package/dist/plan-bundle.js +827 -0
- package/dist/plan-bundle.js.map +1 -0
- package/dist/plan-command.d.ts +29 -0
- package/dist/plan-command.d.ts.map +1 -0
- package/dist/plan-command.js +493 -0
- package/dist/plan-command.js.map +1 -0
- package/dist/plan-contract.d.ts +153 -0
- package/dist/plan-contract.d.ts.map +1 -0
- package/dist/plan-contract.js +61 -0
- package/dist/plan-contract.js.map +1 -0
- package/dist/plan-digest.d.ts +25 -0
- package/dist/plan-digest.d.ts.map +1 -0
- package/dist/plan-digest.js +106 -0
- package/dist/plan-digest.js.map +1 -0
- package/dist/plan-rules.d.ts +23 -0
- package/dist/plan-rules.d.ts.map +1 -0
- package/dist/plan-rules.js +177 -0
- package/dist/plan-rules.js.map +1 -0
- package/dist/planned-bundle.d.ts +20 -0
- package/dist/planned-bundle.d.ts.map +1 -0
- package/dist/planned-bundle.js +191 -0
- package/dist/planned-bundle.js.map +1 -0
- package/dist/product-repository.d.ts +4 -0
- package/dist/product-repository.d.ts.map +1 -1
- package/dist/product-repository.js +9 -1
- package/dist/product-repository.js.map +1 -1
- package/dist/provenance-gate.d.ts +48 -0
- package/dist/provenance-gate.d.ts.map +1 -0
- package/dist/provenance-gate.js +324 -0
- package/dist/provenance-gate.js.map +1 -0
- package/dist/pull-request-body.d.ts +45 -0
- package/dist/pull-request-body.d.ts.map +1 -0
- package/dist/pull-request-body.js +232 -0
- package/dist/pull-request-body.js.map +1 -0
- package/dist/registry-snapshot.d.ts +141 -0
- package/dist/registry-snapshot.d.ts.map +1 -0
- package/dist/registry-snapshot.js +483 -0
- package/dist/registry-snapshot.js.map +1 -0
- package/dist/release-age-edit.d.ts +52 -0
- package/dist/release-age-edit.d.ts.map +1 -0
- package/dist/release-age-edit.js +413 -0
- package/dist/release-age-edit.js.map +1 -0
- package/dist/root-entries.d.ts +36 -0
- package/dist/root-entries.d.ts.map +1 -0
- package/dist/root-entries.js +80 -0
- package/dist/root-entries.js.map +1 -0
- package/dist/setup-template-scripts.d.ts +34 -0
- package/dist/setup-template-scripts.d.ts.map +1 -0
- package/dist/setup-template-scripts.js +557 -0
- package/dist/setup-template-scripts.js.map +1 -0
- package/dist/setup-templates.d.ts +54 -0
- package/dist/setup-templates.d.ts.map +1 -0
- package/dist/setup-templates.js +427 -0
- package/dist/setup-templates.js.map +1 -0
- package/dist/skills.d.ts +34 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +129 -17
- package/dist/skills.js.map +1 -1
- package/dist/status.d.ts +63 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +539 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +151 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -5
- package/skeleton/README.md +14 -9
- package/skeleton/package.json +2 -1
- package/skill/SKILL.md +20 -16
- package/skill-catalogue/advisor/SKILL.md +100 -11
- package/skill-catalogue/architect/SKILL.md +2 -13
- package/skill-catalogue/bouncer/SKILL.md +2 -13
- package/skill-catalogue/builder/SKILL.md +2 -13
- package/skill-catalogue/butler/SKILL.md +2 -13
- package/skill-catalogue/controller/SKILL.md +2 -13
- package/skill-catalogue/customer/SKILL.md +4 -13
- package/skill-catalogue/designer/SKILL.md +9 -14
- package/skill-catalogue/giver/SKILL.md +2 -13
- package/skill-catalogue/influencer/SKILL.md +2 -13
- package/skill-catalogue/inspector/SKILL.md +2 -13
- package/skill-catalogue/integrator/SKILL.md +2 -13
- package/skill-catalogue/keeper/SKILL.md +2 -13
- package/skill-catalogue/launcher/SKILL.md +20 -16
- package/skill-catalogue/locksmith/SKILL.md +2 -13
- package/skill-catalogue/messenger/SKILL.md +2 -13
- package/skill-catalogue/observer/SKILL.md +2 -13
- package/skill-catalogue/publisher/SKILL.md +11 -17
- package/skill-catalogue/starter/SKILL.md +3 -13
- package/skill-catalogue/strategist/SKILL.md +14 -17
- package/skill-catalogue/writer/SKILL.md +7 -14
- package/src/admission-fixture.ts +572 -0
- package/src/admission.ts +816 -0
- package/src/agents-guide.ts +29 -0
- package/src/apply-command-options.check.ts +27 -0
- package/src/apply-plan-cli.ts +454 -14
- package/src/apply-plan.ts +112 -124
- package/src/apply-step-fixture.ts +236 -0
- package/src/apply-store.ts +584 -0
- package/src/approval-sheet.ts +164 -0
- package/src/body-command.ts +162 -0
- package/src/change-set-contract.ts +937 -0
- package/src/change-set-digest.ts +70 -0
- package/src/check-cli.ts +14 -3
- package/src/cli.ts +90 -22
- package/src/core.ts +973 -275
- package/src/dry-materialize.ts +353 -0
- package/src/generated/contract-schema.generated.ts +520 -0
- package/src/generated/package-scope.generated.ts +10 -0
- package/src/generated/plan-contracts.generated.ts +2840 -0
- package/src/host.ts +10 -0
- package/src/identity.ts +51 -0
- package/src/index.ts +72 -3
- package/src/inventory-adoption.ts +107 -29
- package/src/inventory-choice.ts +172 -0
- package/src/inventory-contract.ts +166 -0
- package/src/key-editor.ts +446 -0
- package/src/ledger-contract.ts +637 -0
- package/src/ledger-trust.ts +267 -0
- package/src/lockfile-invariants.ts +421 -0
- package/src/lockfile-readers.ts +749 -0
- package/src/lockfile-regen.ts +851 -0
- package/src/lockfile-tool-env.ts +131 -0
- package/src/materialize.ts +886 -0
- package/src/observe-repository.ts +1365 -0
- package/src/plan-bundle-setup-fixture.ts +200 -0
- package/src/plan-bundle.ts +964 -0
- package/src/plan-command.ts +509 -0
- package/src/plan-contract.ts +179 -0
- package/src/plan-digest.ts +102 -0
- package/src/plan-rules.ts +188 -0
- package/src/planned-bundle.ts +211 -0
- package/src/product-repository.ts +10 -1
- package/src/provenance-gate.ts +352 -0
- package/src/pull-request-body.ts +261 -0
- package/src/registry-snapshot.ts +534 -0
- package/src/release-age-edit.ts +430 -0
- package/src/root-entries.ts +81 -0
- package/src/setup-template-scripts.ts +571 -0
- package/src/setup-templates.ts +471 -0
- package/src/skills.ts +161 -18
- package/src/status.ts +557 -0
- package/src/types.ts +148 -13
- package/CHANGELOG.md +0 -131
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
// AUTO-GENERATED by this package's build-time packer (issue #1475).
|
|
2
|
+
// Do not edit by hand -- edits are overwritten on the next `npm run build`.
|
|
3
|
+
// A byte-identical copy of the contract checker in @clossys/advisor (its
|
|
4
|
+
// source file is not shipped in this package), the one implementation, for a
|
|
5
|
+
// package that has no runtime dependency on @clossys/advisor.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A minimal JSON Schema (draft-07) checker for this repository's shared
|
|
9
|
+
* contracts (issue #1475): the plan record and the engagement brief, which
|
|
10
|
+
* two packages validate against without either depending on the other.
|
|
11
|
+
*
|
|
12
|
+
* It implements exactly the keywords those contracts use, and throws on any
|
|
13
|
+
* other keyword, or keyword form, it meets instead of half-checking it.
|
|
14
|
+
* `assertImplementedContract()` walks a whole contract, so a test can prove
|
|
15
|
+
* every subschema is implemented, visited by a value or not. Property
|
|
16
|
+
* lookups use Object.hasOwn, so an inherited name (toString, constructor,
|
|
17
|
+
* __proto__) is never mistaken for a declared or present property.
|
|
18
|
+
*
|
|
19
|
+
* Messages, paths and errors never echo text from the document under test
|
|
20
|
+
* -- neither a value nor a key. A brief can carry founder text, a refusal
|
|
21
|
+
* can end up in a log, and these messages are read by agents: a hostile
|
|
22
|
+
* document can put prompt-injection text in a key name as easily as in a
|
|
23
|
+
* value. A path names only fields the contract declares (schema text, not
|
|
24
|
+
* document text) and array indices. A field the contract does not declare,
|
|
25
|
+
* or a repeated key, is reported at the object that holds it, by the key's
|
|
26
|
+
* 1-based position in that object ("key 3 of this object"), never by name.
|
|
27
|
+
*
|
|
28
|
+
* Every string, and every object key, must be well-formed Unicode: a lone
|
|
29
|
+
* surrogate is refused whatever the contract says, because it has no UTF-8
|
|
30
|
+
* encoding and so no canonical form. A document this checker accepts can
|
|
31
|
+
* always be digested.
|
|
32
|
+
*
|
|
33
|
+
* `readContractDocument()` is the one way to turn a plan or brief FILE into
|
|
34
|
+
* a value: it refuses bytes that are not valid UTF-8 and any object that
|
|
35
|
+
* repeats a key, at any depth. `JSON.parse` alone would silently keep the
|
|
36
|
+
* last of two duplicate keys, so a reviewer reading the file could see a
|
|
37
|
+
* value that is not the one validated and digested.
|
|
38
|
+
*
|
|
39
|
+
* This file imports nothing. It is the one implementation: a package that
|
|
40
|
+
* cannot import it carries a generated, byte-identical copy.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
export type ContractSchema = Readonly<Record<string, unknown>>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One place a value breaks its contract. `path` is `""` for the document
|
|
47
|
+
* itself, else like `blockers[0].nextAction.who`: only names the contract
|
|
48
|
+
* declares, and array indices. Neither `path` nor `message` ever carries
|
|
49
|
+
* document text.
|
|
50
|
+
*/
|
|
51
|
+
export interface ContractViolation {
|
|
52
|
+
readonly path: string;
|
|
53
|
+
readonly message: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Resolves a file-level `$ref` (for example `engagement-context.json`) to that contract. Throws when it cannot. */
|
|
57
|
+
export type ContractLoader = (name: string) => ContractSchema;
|
|
58
|
+
|
|
59
|
+
const ANNOTATIONS = new Set(["$schema", "$id", "title", "description"]);
|
|
60
|
+
const IMPLEMENTED = new Set([
|
|
61
|
+
"$ref", "type", "const", "enum", "required", "properties", "additionalProperties",
|
|
62
|
+
"items", "minItems", "maxItems", "contains", "minLength", "pattern", "format", "oneOf", "allOf", "not", "definitions",
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The `format` values this checker asserts (JSON Schema leaves `format`
|
|
67
|
+
* optional to assert; these contracts require it). Both are ISO 8601 as
|
|
68
|
+
* RFC 3339 profiles it, checked field by field rather than by shape alone:
|
|
69
|
+
*
|
|
70
|
+
* - `date`: `YYYY-MM-DD`, with month 01-12 and a day that exists in that
|
|
71
|
+
* month of that year (leap years included, so `2028-02-29` is a date and
|
|
72
|
+
* `2026-02-29` is not).
|
|
73
|
+
* - `date-time`: a `date`, then `T`, then `hh:mm` with optional `:ss` and
|
|
74
|
+
* fraction, then `Z` or `+hh:mm` / `-hh:mm`; hours 00-23, minutes 00-59,
|
|
75
|
+
* seconds 00-59 (no leap second), offset hours 00-23 and offset minutes
|
|
76
|
+
* 00-59.
|
|
77
|
+
*
|
|
78
|
+
* Either must also give a finite `Date.parse`, so a time that validates can
|
|
79
|
+
* always be ordered.
|
|
80
|
+
*/
|
|
81
|
+
const FORMATS = new Set(["date", "date-time"]);
|
|
82
|
+
const DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(?:Z|[+-](\d{2}):(\d{2})))?$/;
|
|
83
|
+
|
|
84
|
+
function inRange(text: string | undefined, low: number, high: number): boolean {
|
|
85
|
+
if (text === undefined) return true;
|
|
86
|
+
const value = Number(text);
|
|
87
|
+
return value >= low && value <= high;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function daysInMonth(year: number, month: number): number {
|
|
91
|
+
if (month === 2) return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
|
|
92
|
+
return [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function matchesFormat(format: string, value: string): boolean {
|
|
96
|
+
const match = DATE_TIME.exec(value);
|
|
97
|
+
if (match === null) return false;
|
|
98
|
+
const [, year, month, day, hour, minute, second, offsetHour, offsetMinute] = match;
|
|
99
|
+
const isDateTime = hour !== undefined;
|
|
100
|
+
if (isDateTime !== (format === "date-time")) return false;
|
|
101
|
+
if (!inRange(month, 1, 12)) return false;
|
|
102
|
+
if (!inRange(day, 1, daysInMonth(Number(year), Number(month)))) return false;
|
|
103
|
+
if (!inRange(hour, 0, 23) || !inRange(minute, 0, 59) || !inRange(second, 0, 59)) return false;
|
|
104
|
+
if (!inRange(offsetHour, 0, 23) || !inRange(offsetMinute, 0, 59)) return false;
|
|
105
|
+
return Number.isFinite(Date.parse(value));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Throws on any keyword, or keyword form, this checker does not implement -- for this node only. */
|
|
109
|
+
function assertImplementedNode(schema: ContractSchema, at: string): void {
|
|
110
|
+
for (const key of Object.keys(schema)) {
|
|
111
|
+
if (!ANNOTATIONS.has(key) && !IMPLEMENTED.has(key)) throw new Error(`contract checker does not implement "${key}" (at ${at})`);
|
|
112
|
+
}
|
|
113
|
+
if (Object.hasOwn(schema, "type") && typeof schema.type !== "string") throw new Error(`contract checker implements only a string "type" (at ${at})`);
|
|
114
|
+
if (Object.hasOwn(schema, "additionalProperties") && typeof schema.additionalProperties !== "boolean") {
|
|
115
|
+
throw new Error(`contract checker implements only a boolean "additionalProperties" (at ${at})`);
|
|
116
|
+
}
|
|
117
|
+
if (Object.hasOwn(schema, "items") && Array.isArray(schema.items)) throw new Error(`contract checker does not implement tuple "items" (at ${at})`);
|
|
118
|
+
if (Object.hasOwn(schema, "format") && !FORMATS.has(schema.format as string)) {
|
|
119
|
+
throw new Error(`contract checker does not implement format ${JSON.stringify(schema.format)} (at ${at})`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Walks every subschema of `schema`, visited by a value or not, and throws on the first keyword this checker does not implement. */
|
|
124
|
+
export function assertImplementedContract(schema: ContractSchema, at = "#"): void {
|
|
125
|
+
assertImplementedNode(schema, at);
|
|
126
|
+
for (const key of ["properties", "definitions"]) {
|
|
127
|
+
for (const [name, child] of Object.entries((schema[key] ?? {}) as Record<string, ContractSchema>)) assertImplementedContract(child, `${at}/${key}/${name}`);
|
|
128
|
+
}
|
|
129
|
+
for (const key of ["items", "contains", "not"]) if (Object.hasOwn(schema, key)) assertImplementedContract(schema[key] as ContractSchema, `${at}/${key}`);
|
|
130
|
+
for (const key of ["oneOf", "allOf"]) {
|
|
131
|
+
(Array.isArray(schema[key]) ? (schema[key] as ContractSchema[]) : []).forEach((child, index) => assertImplementedContract(child, `${at}/${key}/${index}`));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Whether an array has a hole: an index below its length with no own element. JSON never produces one. */
|
|
136
|
+
function hasHoles(value: readonly unknown[]): boolean {
|
|
137
|
+
for (let index = 0; index < value.length; index += 1) if (!Object.hasOwn(value, index)) return true;
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The JSON type of a value. An array with holes (`[1, , 3]`, or one whose
|
|
143
|
+
* length was set past its last element) is "sparse array", not "array": code
|
|
144
|
+
* that walks it with forEach skips the holes while an indexed loop reads
|
|
145
|
+
* undefined, so two readers could judge it differently. No JSON text
|
|
146
|
+
* produces one, so a document read from a file is never affected.
|
|
147
|
+
*/
|
|
148
|
+
function typeOf(value: unknown): string {
|
|
149
|
+
if (value === null) return "null";
|
|
150
|
+
if (Array.isArray(value)) return hasHoles(value) ? "sparse array" : "array";
|
|
151
|
+
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
|
|
152
|
+
return typeof value;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** With the `u` flag a surrogate pair is one code point, so this matches only a lone surrogate. */
|
|
156
|
+
const LONE_SURROGATE = /[\uD800-\uDFFF]/u;
|
|
157
|
+
const NOT_WELL_FORMED = "must be well-formed Unicode, and contains a lone surrogate";
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The path of a field the contract declares: `path.name`, or `path["name"]`
|
|
161
|
+
* for a name that is not identifier-like. `name` is always the contract's
|
|
162
|
+
* own text (a `properties` or `required` entry), never a key read only
|
|
163
|
+
* from the document: an undeclared key never reaches this function.
|
|
164
|
+
*/
|
|
165
|
+
function declaredPath(path: string, name: string): string {
|
|
166
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$-]*$/.test(name)) return `${path}[${JSON.stringify(name)}]`;
|
|
167
|
+
return path === "" ? name : `${path}.${name}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Each object's keys in the order the file wrote them, for a value
|
|
172
|
+
* `readContractDocument()` returned. A JavaScript object enumerates
|
|
173
|
+
* array-index keys ("0", "7") first, whatever order they were written in,
|
|
174
|
+
* so without this a key ordinal could differ from the file's.
|
|
175
|
+
*/
|
|
176
|
+
const WRITTEN_KEY_ORDER = new WeakMap<object, readonly string[]>();
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* `record`'s keys in the order the checker visits them, which is also the
|
|
180
|
+
* order their 1-based positions count in: as the file wrote them when
|
|
181
|
+
* `record` came from `readContractDocument()` and still has exactly the keys
|
|
182
|
+
* it was read with, else the object's own key order (JavaScript's, which
|
|
183
|
+
* lists array-index keys such as "7" first). Linear in the number of keys.
|
|
184
|
+
*/
|
|
185
|
+
function keyOrder(record: object): readonly string[] {
|
|
186
|
+
const own = Object.keys(record);
|
|
187
|
+
const written = WRITTEN_KEY_ORDER.get(record);
|
|
188
|
+
if (written !== undefined && written.length === own.length) {
|
|
189
|
+
const writtenSet = new Set(written);
|
|
190
|
+
if (own.every((key) => writtenSet.has(key))) return written;
|
|
191
|
+
}
|
|
192
|
+
return own;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const TYPE_NOUNS: Readonly<Record<string, string>> = {
|
|
196
|
+
object: "an object", array: "an array", string: "a string", number: "a number", integer: "an integer", boolean: "a boolean", null: "null",
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* What a value must be, for a type mismatch. A `title` that is already a
|
|
201
|
+
* description ("a string with ...") is used as it is; one that names a
|
|
202
|
+
* document ("Advisor plan") is added after the type: "an object (the
|
|
203
|
+
* Advisor plan)".
|
|
204
|
+
*/
|
|
205
|
+
function describeType(type: string, title: unknown): string {
|
|
206
|
+
if (typeof title !== "string") return TYPE_NOUNS[type] ?? `of type ${type}`;
|
|
207
|
+
if (/^(a|an) /.test(title)) return title;
|
|
208
|
+
return `${TYPE_NOUNS[type] ?? `of type ${type}`} (the ${title})`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
interface Scope {
|
|
212
|
+
readonly root: ContractSchema;
|
|
213
|
+
readonly load: ContractLoader;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function resolveRef(ref: string, scope: Scope, at: string): { schema: ContractSchema; scope: Scope } {
|
|
217
|
+
const [file = "", pointer = ""] = ref.split("#");
|
|
218
|
+
const docRoot = file === "" ? scope.root : scope.load(file);
|
|
219
|
+
let target: unknown = docRoot;
|
|
220
|
+
for (const segment of pointer.split("/").filter(Boolean)) {
|
|
221
|
+
if (typeof target !== "object" || target === null || !Object.hasOwn(target, segment)) throw new Error(`unresolvable $ref ${ref} (at ${at})`);
|
|
222
|
+
target = (target as Record<string, unknown>)[segment];
|
|
223
|
+
}
|
|
224
|
+
return { schema: target as ContractSchema, scope: { root: docRoot, load: scope.load } };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Follows `$ref` chains to the node that actually carries keywords. */
|
|
228
|
+
function dereference(schema: ContractSchema, scope: Scope, at: string): { schema: ContractSchema; scope: Scope } {
|
|
229
|
+
let current = { schema, scope };
|
|
230
|
+
while (typeof current.schema.$ref === "string") current = resolveRef(current.schema.$ref, current.scope, at);
|
|
231
|
+
return current;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** An undeclared field, by its 1-based position in its object -- never its name. */
|
|
235
|
+
function undeclaredFieldMessage(ordinal: number): string {
|
|
236
|
+
return `has a field the contract does not declare (key ${ordinal} of this object), and unknown fields are refused`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function check(input: ContractSchema, value: unknown, inputScope: Scope, path: string): ContractViolation[] {
|
|
240
|
+
// Draft-07: $ref replaces every sibling keyword (siblings are annotations only).
|
|
241
|
+
const { schema, scope } = dereference(input, inputScope, path);
|
|
242
|
+
assertImplementedNode(schema, path);
|
|
243
|
+
const kind = typeOf(value);
|
|
244
|
+
const expected = schema.type;
|
|
245
|
+
if (typeof expected === "string" && !(expected === kind || (expected === "number" && kind === "integer"))) {
|
|
246
|
+
return [{ path, message: `must be ${describeType(expected, schema.title)}, got ${kind}` }];
|
|
247
|
+
}
|
|
248
|
+
// A schema node with no `type` still never accepts an array with holes.
|
|
249
|
+
if (kind === "sparse array") return [{ path, message: "must not be an array with holes" }];
|
|
250
|
+
const violations: ContractViolation[] = [];
|
|
251
|
+
if (Object.hasOwn(schema, "const") && JSON.stringify(schema.const) !== JSON.stringify(value)) violations.push({ path, message: `must equal ${JSON.stringify(schema.const)}` });
|
|
252
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((option) => JSON.stringify(option) === JSON.stringify(value))) {
|
|
253
|
+
violations.push({ path, message: `must be one of: ${schema.enum.map((option) => JSON.stringify(option)).join(", ")}` });
|
|
254
|
+
}
|
|
255
|
+
if (typeof value === "string") {
|
|
256
|
+
if (LONE_SURROGATE.test(value)) violations.push({ path, message: NOT_WELL_FORMED });
|
|
257
|
+
if (typeof schema.minLength === "number" && value.length < schema.minLength) violations.push({ path, message: `must be at least ${schema.minLength} character(s) long` });
|
|
258
|
+
if (typeof schema.pattern === "string" && !new RegExp(schema.pattern, "u").test(value)) {
|
|
259
|
+
violations.push({ path, message: typeof schema.title === "string" ? `must be ${schema.title}` : `must match the pattern ${schema.pattern}` });
|
|
260
|
+
}
|
|
261
|
+
if (typeof schema.format === "string" && !matchesFormat(schema.format, value)) {
|
|
262
|
+
violations.push({ path, message: typeof schema.title === "string" ? `must be ${schema.title}` : `must be a valid ${schema.format}` });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (kind === "array") {
|
|
266
|
+
const items = value as readonly unknown[];
|
|
267
|
+
if (typeof schema.minItems === "number" && items.length < schema.minItems) violations.push({ path, message: `must have at least ${schema.minItems} item(s)` });
|
|
268
|
+
if (typeof schema.maxItems === "number" && items.length > schema.maxItems) violations.push({ path, message: `must have at most ${schema.maxItems} item(s)` });
|
|
269
|
+
if (schema.items !== undefined) items.forEach((item, index) => violations.push(...check(schema.items as ContractSchema, item, scope, `${path}[${index}]`)));
|
|
270
|
+
if (schema.contains !== undefined && !items.some((item, index) => check(schema.contains as ContractSchema, item, scope, `${path}[${index}]`).length === 0)) {
|
|
271
|
+
violations.push({ path, message: `must contain an item matching ${JSON.stringify(schema.contains)}` });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (kind === "object") {
|
|
275
|
+
const record = value as Record<string, unknown>;
|
|
276
|
+
const properties = (schema.properties ?? {}) as Record<string, ContractSchema>;
|
|
277
|
+
for (const name of (schema.required ?? []) as string[]) {
|
|
278
|
+
if (!Object.hasOwn(record, name)) violations.push({ path: declaredPath(path, name), message: "is required" });
|
|
279
|
+
}
|
|
280
|
+
keyOrder(record).forEach((name, index) => {
|
|
281
|
+
const child = record[name];
|
|
282
|
+
if (LONE_SURROGATE.test(name)) {
|
|
283
|
+
// The key itself is never echoed (see the header), and this one cannot even be written as UTF-8.
|
|
284
|
+
violations.push({ path, message: `has a key that ${NOT_WELL_FORMED}` });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (Object.hasOwn(properties, name)) violations.push(...check(properties[name] as ContractSchema, child, scope, declaredPath(path, name)));
|
|
288
|
+
// The key is document text, so it is never named: the violation is
|
|
289
|
+
// placed at the object that holds it, with the key's position there.
|
|
290
|
+
else if (schema.additionalProperties === false) violations.push({ path, message: undeclaredFieldMessage(index + 1) });
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (Array.isArray(schema.oneOf)) violations.push(...checkOneOf(schema.oneOf as ContractSchema[], value, scope, path, schema.title));
|
|
294
|
+
if (Array.isArray(schema.allOf)) for (const branch of schema.allOf as ContractSchema[]) violations.push(...check(branch, value, scope, path));
|
|
295
|
+
if (schema.not !== undefined && check(schema.not as ContractSchema, value, scope, path).length === 0) violations.push({ path, message: `must not match ${JSON.stringify(schema.not)}` });
|
|
296
|
+
return violations;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Exactly one branch must pass. When none does, the violations of the one
|
|
301
|
+
* closest branch are reported -- the branch whose `type` fits the value, or
|
|
302
|
+
* failing that the unique branch with the fewest violations -- so a refusal
|
|
303
|
+
* names the actual field at fault rather than only "no branch matched".
|
|
304
|
+
* When no single branch is closest, the node's own `title` says what the
|
|
305
|
+
* value must be, if it has one.
|
|
306
|
+
*/
|
|
307
|
+
function checkOneOf(branches: readonly ContractSchema[], value: unknown, scope: Scope, path: string, title: unknown): ContractViolation[] {
|
|
308
|
+
const results = branches.map((branch) => check(branch, value, scope, path));
|
|
309
|
+
const passing = results.filter((result) => result.length === 0).length;
|
|
310
|
+
if (passing === 1) return [];
|
|
311
|
+
if (passing > 1) return [{ path, message: `matches ${passing} of the ${branches.length} allowed forms, and must match exactly one` }];
|
|
312
|
+
const kind = typeOf(value);
|
|
313
|
+
const typed = branches
|
|
314
|
+
.map((branch, index) => ({ index, type: dereference(branch, scope, path).schema.type }))
|
|
315
|
+
.filter((entry) => entry.type === kind || (entry.type === "number" && kind === "integer"));
|
|
316
|
+
if (typed.length === 1) return results[typed[0]!.index]!;
|
|
317
|
+
const fewest = Math.min(...results.map((result) => result.length));
|
|
318
|
+
const closest = results.filter((result) => result.length === fewest);
|
|
319
|
+
if (closest.length === 1) return closest[0]!;
|
|
320
|
+
if (typeof title === "string") return [{ path, message: `must be ${title}` }];
|
|
321
|
+
return [{ path, message: `does not match any of the ${branches.length} allowed forms` }];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Every violation of `value` against `contract`; empty when it conforms. A
|
|
326
|
+
* type mismatch stops there and returns alone, since every other keyword
|
|
327
|
+
* assumes the value is already the right type. Otherwise, on an object, any
|
|
328
|
+
* `const` or `enum` violation of the object itself comes before a missing
|
|
329
|
+
* required field, which comes before each present key's violations in the
|
|
330
|
+
* order the keys were written (see `keyOrder()`), then any from `oneOf`,
|
|
331
|
+
* `allOf` and `not`. Never throws for a bad value. Throws only when the
|
|
332
|
+
* contract itself uses a keyword this checker does not implement, or a
|
|
333
|
+
* `$ref` it cannot resolve -- a defect in the contract, not in the value.
|
|
334
|
+
*/
|
|
335
|
+
export function validateAgainstContract(contract: ContractSchema, value: unknown, load: ContractLoader): ContractViolation[] {
|
|
336
|
+
return check(contract, value, { root: contract, load }, "");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** `label.path message` (for example `plan.blockers[0].capabilityId is required`), or `label message` for the document itself. */
|
|
340
|
+
export function formatContractViolation(label: string, violation: ContractViolation): string {
|
|
341
|
+
const where = violation.path === "" ? label : violation.path.startsWith("[") ? `${label}${violation.path}` : `${label}.${violation.path}`;
|
|
342
|
+
return `${where} ${violation.message}`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ignoreBOM keeps a leading byte order mark in the text, so it is refused below rather than silently stripped.
|
|
346
|
+
const STRICT_UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
347
|
+
const JSON_WHITESPACE = " \t\n\r";
|
|
348
|
+
const JSON_NUMBER = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
|
|
349
|
+
const JSON_ESCAPE = /\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4})/y;
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Why `readContractDocument()` refused a file, as data a caller can act on
|
|
353
|
+
* without parsing the message: `reason` says which rule failed, and
|
|
354
|
+
* `position` (never file text) is set only for a syntax error or a leading
|
|
355
|
+
* byte order mark. The message carries no file text either: a repeated key
|
|
356
|
+
* is named by its 1-based position in its object, and that object as the
|
|
357
|
+
* top-level object or by its position, never by any key.
|
|
358
|
+
*
|
|
359
|
+
* A position, here and in every message, is a 0-based index into the
|
|
360
|
+
* decoded text in UTF-16 code units: JavaScript's string index. It equals
|
|
361
|
+
* the byte offset only for ASCII text; a character outside the Basic
|
|
362
|
+
* Multilingual Plane (an emoji, say) counts as two.
|
|
363
|
+
*/
|
|
364
|
+
export class ContractDocumentError extends Error {
|
|
365
|
+
constructor(
|
|
366
|
+
message: string,
|
|
367
|
+
readonly reason: "encoding" | "syntax" | "repeated-key",
|
|
368
|
+
readonly position?: number,
|
|
369
|
+
) {
|
|
370
|
+
super(message);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Checks that `text` is exactly one JSON value (RFC 8259 grammar) with no
|
|
376
|
+
* object that repeats a key, at any depth. Keys are compared after
|
|
377
|
+
* unescaping, so `"a"` and `"\u0061"` are the same key. Every refusal is
|
|
378
|
+
* by position only, never with a snippet of the text or a key, because a
|
|
379
|
+
* plan or brief can carry founder prose and a key can carry anything.
|
|
380
|
+
*
|
|
381
|
+
* Returns every object's keys in the order written, one list per object in
|
|
382
|
+
* document (pre-)order, for `recordWrittenKeyOrder()`.
|
|
383
|
+
*/
|
|
384
|
+
function checkStrictJson(text: string): string[][] {
|
|
385
|
+
const keyOrders: string[][] = [];
|
|
386
|
+
let index = 0;
|
|
387
|
+
const syntaxError = (): never => {
|
|
388
|
+
throw new ContractDocumentError(`is not valid JSON at position ${index}`, "syntax", index);
|
|
389
|
+
};
|
|
390
|
+
const skipWhitespace = () => {
|
|
391
|
+
while (index < text.length && JSON_WHITESPACE.includes(text[index]!)) index += 1;
|
|
392
|
+
};
|
|
393
|
+
const expect = (character: string) => {
|
|
394
|
+
if (text[index] !== character) syntaxError();
|
|
395
|
+
index += 1;
|
|
396
|
+
};
|
|
397
|
+
const readString = (): string => {
|
|
398
|
+
const start = index;
|
|
399
|
+
expect('"');
|
|
400
|
+
for (;;) {
|
|
401
|
+
if (index >= text.length) syntaxError();
|
|
402
|
+
const unit = text.charCodeAt(index);
|
|
403
|
+
if (unit === 0x22) break;
|
|
404
|
+
if (unit < 0x20) syntaxError();
|
|
405
|
+
if (unit === 0x5c) {
|
|
406
|
+
JSON_ESCAPE.lastIndex = index;
|
|
407
|
+
if (!JSON_ESCAPE.test(text)) syntaxError();
|
|
408
|
+
index = JSON_ESCAPE.lastIndex;
|
|
409
|
+
} else {
|
|
410
|
+
index += 1;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
index += 1;
|
|
414
|
+
return JSON.parse(text.slice(start, index)) as string;
|
|
415
|
+
};
|
|
416
|
+
const scan = (topLevel: boolean): void => {
|
|
417
|
+
skipWhitespace();
|
|
418
|
+
const opening = text[index];
|
|
419
|
+
if (opening === "{" || opening === "[") {
|
|
420
|
+
const closing = opening === "{" ? "}" : "]";
|
|
421
|
+
const where = topLevel ? "the top-level object" : `the object at position ${index}`;
|
|
422
|
+
const keys: string[] = [];
|
|
423
|
+
if (opening === "{") keyOrders.push(keys);
|
|
424
|
+
const seen = new Set<string>();
|
|
425
|
+
index += 1;
|
|
426
|
+
skipWhitespace();
|
|
427
|
+
if (text[index] === closing) {
|
|
428
|
+
index += 1;
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
for (;;) {
|
|
432
|
+
if (opening === "{") {
|
|
433
|
+
skipWhitespace();
|
|
434
|
+
const key = readString();
|
|
435
|
+
keys.push(key);
|
|
436
|
+
if (seen.has(key)) {
|
|
437
|
+
// Never the key itself: it is document text, and may be written to be read as an instruction.
|
|
438
|
+
throw new ContractDocumentError(`repeats a key (key ${keys.length} of ${where}); every key may appear once`, "repeated-key");
|
|
439
|
+
}
|
|
440
|
+
seen.add(key);
|
|
441
|
+
skipWhitespace();
|
|
442
|
+
expect(":");
|
|
443
|
+
}
|
|
444
|
+
scan(false);
|
|
445
|
+
skipWhitespace();
|
|
446
|
+
if (text[index] === ",") {
|
|
447
|
+
index += 1;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
expect(closing);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (opening === '"') {
|
|
455
|
+
readString();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
for (const literal of ["true", "false", "null"]) {
|
|
459
|
+
if (text.startsWith(literal, index)) {
|
|
460
|
+
index += literal.length;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
JSON_NUMBER.lastIndex = index;
|
|
465
|
+
if (!JSON_NUMBER.test(text)) syntaxError();
|
|
466
|
+
index = JSON_NUMBER.lastIndex;
|
|
467
|
+
};
|
|
468
|
+
scan(true);
|
|
469
|
+
skipWhitespace();
|
|
470
|
+
if (index !== text.length) syntaxError();
|
|
471
|
+
return keyOrders;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Records, for each object in `value`, the key order `checkStrictJson()`
|
|
476
|
+
* read for it. `keyOrders` lists objects in document order, which is the
|
|
477
|
+
* order this walk meets them in: each object, then its members in the order
|
|
478
|
+
* written. Keys are unique (a repeat was already refused), so each names
|
|
479
|
+
* exactly one own property; `value[key]` reads an own `__proto__` too,
|
|
480
|
+
* because JSON.parse defines it as an own property that shadows the
|
|
481
|
+
* inherited accessor.
|
|
482
|
+
*/
|
|
483
|
+
function recordWrittenKeyOrder(value: unknown, keyOrders: readonly string[][], next: { index: number }): void {
|
|
484
|
+
if (typeof value !== "object" || value === null) return;
|
|
485
|
+
if (Array.isArray(value)) {
|
|
486
|
+
for (const item of value) recordWrittenKeyOrder(item, keyOrders, next);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const keys = keyOrders[next.index]!;
|
|
490
|
+
next.index += 1;
|
|
491
|
+
WRITTEN_KEY_ORDER.set(value, keys);
|
|
492
|
+
for (const key of keys) recordWrittenKeyOrder((value as Record<string, unknown>)[key], keyOrders, next);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Reads a plan or brief file's bytes as strict JSON: UTF-8 that decodes
|
|
497
|
+
* without error (never silently replaced with U+FFFD) and does not start
|
|
498
|
+
* with a byte order mark, exactly one JSON value, and no object that repeats a key at any depth -- the I-JSON rules
|
|
499
|
+
* RFC 8785 canonicalization assumes. Refuses bytes that break a rule with a
|
|
500
|
+
* ContractDocumentError whose message says which rule, by position only: a
|
|
501
|
+
* syntax error by its position (a UTF-16 code-unit index, see
|
|
502
|
+
* ContractDocumentError), a repeated key by its 1-based position in its
|
|
503
|
+
* object and that object's position (or "the top-level object") -- never
|
|
504
|
+
* any text from the file. It does not validate the value against a contract; call
|
|
505
|
+
* `validateAgainstContract()` next, which then numbers an undeclared field
|
|
506
|
+
* by its position as written in the file.
|
|
507
|
+
*/
|
|
508
|
+
export function readContractDocument(bytes: Uint8Array): unknown {
|
|
509
|
+
let text: string;
|
|
510
|
+
try {
|
|
511
|
+
text = STRICT_UTF8.decode(bytes);
|
|
512
|
+
} catch {
|
|
513
|
+
throw new ContractDocumentError("is not valid UTF-8", "encoding");
|
|
514
|
+
}
|
|
515
|
+
if (text.charCodeAt(0) === 0xfeff) throw new ContractDocumentError("is not valid JSON at position 0: it starts with a byte order mark, which strict JSON refuses", "syntax", 0);
|
|
516
|
+
const keyOrders = checkStrictJson(text);
|
|
517
|
+
const value = JSON.parse(text) as unknown;
|
|
518
|
+
recordWrittenKeyOrder(value, keyOrders, { index: 0 });
|
|
519
|
+
return value;
|
|
520
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// AUTO-GENERATED by this package's build-time packer (issue #1178).
|
|
2
|
+
// Do not edit by hand -- edits are overwritten on the next `npm run build`.
|
|
3
|
+
// Source of truth: the one file in this package's source repository that sets
|
|
4
|
+
// the publishing scope and registry (not shipped in this package).
|
|
5
|
+
|
|
6
|
+
/** The publishing scope and the registry packages are published to and read from. */
|
|
7
|
+
export const PACKAGE_SCOPE: { readonly scope: string; readonly registry: string } = {
|
|
8
|
+
"scope": "@clossys",
|
|
9
|
+
"registry": "https://registry.npmjs.org"
|
|
10
|
+
};
|