phasegate 0.229.0 → 0.264.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/CHANGELOG.md +73 -1
- package/README.ja.md +31 -1
- package/README.md +30 -0
- package/docs/ADR/031-world-model-ownership-and-corpus-lifecycle.md +155 -0
- package/docs/ADR/032-world-node-identity.md +198 -0
- package/docs/ADR/033-world-snapshot-canonicalization.md +246 -0
- package/docs/ADR/034-world-constraint-semantics.md +218 -0
- package/docs/ADR/035-world-adoption-baseline-and-waiver.md +341 -0
- package/docs/ADR/036-world-model-and-doc-freshness.md +169 -0
- package/docs/ADR/037-world-cli-and-output-contract.md +398 -0
- package/docs/contracts/attestation-v2.schema.json +110 -0
- package/docs/contracts/requirement-test-matrix.schema.json +15 -0
- package/docs/contracts/world-baseline.schema.json +35 -0
- package/docs/contracts/world-constraints.schema.json +56 -0
- package/docs/contracts/world-debts.schema.json +32 -0
- package/docs/contracts/world-obligation-report.schema.json +259 -0
- package/docs/contracts/world-waivers.schema.json +32 -0
- package/docs/guide/cli-reference.md +45 -0
- package/docs/guide/configuration.md +56 -0
- package/docs/templates/ci/aidlc-gate.yml +42 -2
- package/package.json +1 -1
- package/scripts/harness/agent-integration/application/dto/open-world-obligations-context-dto.ts +30 -0
- package/scripts/harness/agent-integration/application/ports/world-obligations-query-port.ts +30 -0
- package/scripts/harness/agent-integration/application/usecases/get-open-world-obligations-context-usecase.ts +66 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/world-model-open-obligations-query-adapter.ts +58 -0
- package/scripts/harness/agent-integration/presentation/session-start-hook.ts +37 -1
- package/scripts/harness/agent-integration/presentation/world-obligations-session-context.ts +60 -0
- package/scripts/harness/attestation/application/dto/attestation-document.ts +17 -4
- package/scripts/harness/attestation/application/mappers/attestation-record-mapper.ts +55 -4
- package/scripts/harness/attestation/application/ports/sha256-capability.ts +25 -0
- package/scripts/harness/attestation/application/ports/world-snapshot-root-provider.ts +10 -0
- package/scripts/harness/attestation/application/usecases/produce-attestation-usecase.ts +33 -7
- package/scripts/harness/attestation/composition-root.ts +19 -4
- package/scripts/harness/attestation/domain/entities/attestation-record.ts +37 -2
- package/scripts/harness/attestation/index.ts +13 -2
- package/scripts/harness/attestation/infrastructure/adapters/node-crypto-content-hasher-adapter.ts +6 -6
- package/scripts/harness/attestation/infrastructure/adapters/node-crypto-sha256-capability.ts +19 -0
- package/scripts/harness/attestation/presentation/handlers/attest-handler.ts +5 -2
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +35 -8
- package/scripts/harness/config-foundation/application/mappers/world-model-config-mapper.ts +15 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +67 -87
- package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +77 -88
- package/scripts/harness/config-foundation/domain/value-objects/world-config.ts +198 -0
- package/scripts/harness/config-foundation/index.ts +14 -15
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +24 -0
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +24 -0
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +24 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +83 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +83 -0
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +18 -4
- package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +4 -1
- package/scripts/harness/integrations/pre-commit.ts +169 -27
- package/scripts/harness/main.ts +276 -126
- package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +11 -0
- package/scripts/harness/nyquist-validation/application/usecases/check-ac-coverage-gate-usecase.ts +6 -0
- package/scripts/harness/nyquist-validation/application/usecases/generate-requirement-test-matrix-usecase.ts +8 -4
- package/scripts/harness/nyquist-validation/domain/entities/story-mapping.ts +29 -2
- package/scripts/harness/nyquist-validation/domain/services/ac-coverage-gate-policy.ts +28 -0
- package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +71 -4
- package/scripts/harness/nyquist-validation/infrastructure/schema/matrix-schema-loader.ts +4 -0
- package/scripts/harness/traceability-model/application/dto/changed-design-fragment-dto.ts +31 -0
- package/scripts/harness/traceability-model/application/dto/traceability-world-read-dto.ts +67 -0
- package/scripts/harness/traceability-model/application/facades/design-change-read-facade.ts +14 -0
- package/scripts/harness/traceability-model/application/facades/traceability-world-read-facade.ts +372 -0
- package/scripts/harness/traceability-model/application/ports/staged-design-change-source-port.ts +9 -0
- package/scripts/harness/traceability-model/application/ports/traceability-world-read-source-port.ts +61 -0
- package/scripts/harness/traceability-model/composition-root.ts +26 -5
- package/scripts/harness/traceability-model/index.ts +26 -6
- package/scripts/harness/traceability-model/infrastructure/adapters/file-system-traceability-world-read-adapter.ts +223 -0
- package/scripts/harness/traceability-model/infrastructure/adapters/git-staged-design-change-adapter.ts +155 -0
- package/scripts/harness/traceability-model/infrastructure/parsers/story-catalog-parser.ts +86 -8
- package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +31 -0
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +34 -0
- package/scripts/harness/validator-system/composition-root.ts +21 -6
- package/scripts/harness/validator-system/domain/ports/world-constraint-admission-policy-port.ts +28 -0
- package/scripts/harness/validator-system/domain/ports/world-constraint-rederivation-policy-port.ts +11 -0
- package/scripts/harness/validator-system/domain/services/design-change-declaration-policy.ts +75 -0
- package/scripts/harness/validator-system/domain/services/world-constraint-admission-service.ts +78 -0
- package/scripts/harness/validator-system/domain/services/world-constraint-rederivation-service.ts +76 -0
- package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +4 -0
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +38 -16
- package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-admission-adapter.ts +56 -0
- package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-rederivation-adapter.ts +56 -0
- package/scripts/harness/world-model/application/dto/pinned-design-endpoint-dto.ts +19 -0
- package/scripts/harness/world-model/application/dto/world-inspection-dto.ts +43 -0
- package/scripts/harness/world-model/application/dto/world-obligation-report-dto.ts +54 -0
- package/scripts/harness/world-model/application/dto/world-resolved-config-input.ts +127 -0
- package/scripts/harness/world-model/application/dto/world-snapshot-root-dto.ts +8 -0
- package/scripts/harness/world-model/application/facades/pinned-design-endpoint-facade.ts +63 -0
- package/scripts/harness/world-model/application/facades/world-snapshot-root-facade.ts +19 -0
- package/scripts/harness/world-model/application/ports/obligation-report-writer-port.ts +7 -0
- package/scripts/harness/world-model/application/ports/world-control-declaration-repository-port.ts +57 -0
- package/scripts/harness/world-model/application/ports/world-fact-source-port.ts +17 -0
- package/scripts/harness/world-model/application/usecases/build-snapshot-use-case.ts +111 -0
- package/scripts/harness/world-model/application/usecases/derive-obligations-use-case.ts +155 -0
- package/scripts/harness/world-model/application/usecases/derive-world-obligations-use-case.ts +146 -0
- package/scripts/harness/world-model/application/usecases/inspect-world-use-case.ts +74 -0
- package/scripts/harness/world-model/application/usecases/pin-constraint-endpoint-use-case.ts +139 -0
- package/scripts/harness/world-model/composition-root.ts +247 -0
- package/scripts/harness/world-model/domain/entities/constraint-record.ts +148 -0
- package/scripts/harness/world-model/domain/entities/control-declarations.ts +274 -0
- package/scripts/harness/world-model/domain/entities/edge.ts +41 -0
- package/scripts/harness/world-model/domain/entities/extraction-diagnostic.ts +60 -0
- package/scripts/harness/world-model/domain/entities/snapshot.ts +75 -0
- package/scripts/harness/world-model/domain/entities/world-node.ts +210 -0
- package/scripts/harness/world-model/domain/ports/world-hashing-port.ts +8 -0
- package/scripts/harness/world-model/domain/services/canonical-json-serializer.ts +113 -0
- package/scripts/harness/world-model/domain/services/constraint-evaluator.ts +486 -0
- package/scripts/harness/world-model/domain/services/obligation-derivation-service.ts +199 -0
- package/scripts/harness/world-model/domain/services/policy-inputs-digest-deriver.ts +66 -0
- package/scripts/harness/world-model/domain/services/snapshot-root-deriver.ts +218 -0
- package/scripts/harness/world-model/domain/services/text-content-normalizer.ts +30 -0
- package/scripts/harness/world-model/domain/services/violation-fingerprint-deriver.ts +183 -0
- package/scripts/harness/world-model/domain/value-objects/artifact-kind.ts +58 -0
- package/scripts/harness/world-model/domain/value-objects/change-provenance.ts +137 -0
- package/scripts/harness/world-model/domain/value-objects/corpus-role.ts +57 -0
- package/scripts/harness/world-model/domain/value-objects/declared-key.ts +35 -0
- package/scripts/harness/world-model/domain/value-objects/evaluation-id.ts +41 -0
- package/scripts/harness/world-model/domain/value-objects/explicit-constraint-relation.ts +53 -0
- package/scripts/harness/world-model/domain/value-objects/explicit-node-alias.ts +30 -0
- package/scripts/harness/world-model/domain/value-objects/node-pin.ts +34 -0
- package/scripts/harness/world-model/domain/value-objects/path-key.ts +103 -0
- package/scripts/harness/world-model/domain/value-objects/sha256-digest.ts +39 -0
- package/scripts/harness/world-model/domain/value-objects/violation-fingerprint.ts +36 -0
- package/scripts/harness/world-model/domain/value-objects/wcr-rule-id.ts +59 -0
- package/scripts/harness/world-model/domain/value-objects/world-node-id.ts +222 -0
- package/scripts/harness/world-model/index.ts +33 -0
- package/scripts/harness/world-model/infrastructure/adapters/adr-fact-extractor.ts +27 -0
- package/scripts/harness/world-model/infrastructure/adapters/assembled-world-fact-source.ts +17 -0
- package/scripts/harness/world-model/infrastructure/adapters/attestation-fact-extractor.ts +323 -0
- package/scripts/harness/world-model/infrastructure/adapters/attestation-sha256-world-hashing-adapter.ts +15 -0
- package/scripts/harness/world-model/infrastructure/adapters/composite-design-fact-source.ts +23 -0
- package/scripts/harness/world-model/infrastructure/adapters/design-corpus-fact-extractor.ts +285 -0
- package/scripts/harness/world-model/infrastructure/adapters/design-fact-extraction.ts +63 -0
- package/scripts/harness/world-model/infrastructure/adapters/file-system-obligation-report-writer-adapter.ts +37 -0
- package/scripts/harness/world-model/infrastructure/adapters/file-system-world-control-repository-adapters.ts +332 -0
- package/scripts/harness/world-model/infrastructure/adapters/integrity-manifest-fact-extractor.ts +101 -0
- package/scripts/harness/world-model/infrastructure/adapters/json-fact-extractor-support.ts +151 -0
- package/scripts/harness/world-model/infrastructure/adapters/markdown-design-fact-extractor.ts +493 -0
- package/scripts/harness/world-model/infrastructure/adapters/matrix-fact-extractor.ts +283 -0
- package/scripts/harness/world-model/infrastructure/adapters/product-fact-extractor.ts +29 -0
- package/scripts/harness/world-model/infrastructure/adapters/proposal-fact-extractor.ts +29 -0
- package/scripts/harness/world-model/infrastructure/adapters/runtime-fact-extraction.ts +19 -0
- package/scripts/harness/world-model/infrastructure/adapters/source-metadata-fact-extractor.ts +22 -0
- package/scripts/harness/world-model/infrastructure/adapters/test-reference-source-fact-extractor.ts +22 -0
- package/scripts/harness/world-model/infrastructure/adapters/traceability-design-fact-adapter.ts +112 -0
- package/scripts/harness/world-model/infrastructure/adapters/traceability-world-read-facade-merger.ts +38 -0
- package/scripts/harness/world-model/infrastructure/adapters/type-script-source-fact-extractor.ts +236 -0
- package/scripts/harness/world-model/infrastructure/adapters/unit-fact-extractor.ts +71 -0
- package/scripts/harness/world-model/infrastructure/adapters/world-control-declaration-mapper.ts +383 -0
- package/scripts/harness/world-model/presentation/cli/world-command-support.ts +60 -0
- package/scripts/harness/world-model/presentation/cli/world-derive-command-handler.ts +109 -0
- package/scripts/harness/world-model/presentation/cli/world-inspect-command-handler.ts +194 -0
- package/scripts/harness/world-model/presentation/cli/world-pin-command-handler.ts +100 -0
package/scripts/harness/world-model/infrastructure/adapters/type-script-source-fact-extractor.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// @unit world-model
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-290, WI-291
|
|
4
|
+
|
|
5
|
+
import type { Dirent } from "node:fs";
|
|
6
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { ExtractionDiagnostic } from "../../domain/entities/extraction-diagnostic.js";
|
|
9
|
+
import { WorldNode } from "../../domain/entities/world-node.js";
|
|
10
|
+
import type { WorldHashingPort } from "../../domain/ports/world-hashing-port.js";
|
|
11
|
+
import { TextContentNormalizer } from "../../domain/services/text-content-normalizer.js";
|
|
12
|
+
import { PathKey } from "../../domain/value-objects/path-key.js";
|
|
13
|
+
import type { RuntimeFactExtraction } from "./runtime-fact-extraction.js";
|
|
14
|
+
|
|
15
|
+
const UNIT_TAG = /^\s*(?:\/\/|\*)\s*@unit\s+(.+?)(?:\s*\*\/)?\s*$/;
|
|
16
|
+
const LAYER_TAG = /^\s*(?:\/\/|\*)\s*@layer\s+(.+?)(?:\s*\*\/)?\s*$/;
|
|
17
|
+
const WORK_ITEM_TAG = /^\s*(?:\/\/|\*)\s*@work-item-id\s+(.+?)(?:\s*\*\/)?\s*$/;
|
|
18
|
+
|
|
19
|
+
const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
|
|
20
|
+
|
|
21
|
+
export type TypeScriptSourceKind = "implementation" | "test";
|
|
22
|
+
|
|
23
|
+
export interface TypeScriptSourceFactExtractorDeps {
|
|
24
|
+
readonly rootDir: string;
|
|
25
|
+
readonly hashingPort: WorldHashingPort;
|
|
26
|
+
readonly sourceKind: TypeScriptSourceKind;
|
|
27
|
+
readonly sourceRoot?: string;
|
|
28
|
+
readonly include: (relativePath: string) => boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const splitValues = (value: string): readonly string[] =>
|
|
32
|
+
value
|
|
33
|
+
.split(",")
|
|
34
|
+
.map((item) => item.trim())
|
|
35
|
+
.filter((item) => item.length > 0);
|
|
36
|
+
|
|
37
|
+
export class TypeScriptSourceFactExtractor {
|
|
38
|
+
private readonly deps: TypeScriptSourceFactExtractorDeps;
|
|
39
|
+
private readonly normalizer = new TextContentNormalizer();
|
|
40
|
+
|
|
41
|
+
constructor(deps: TypeScriptSourceFactExtractorDeps) {
|
|
42
|
+
this.deps = deps;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async extract(): Promise<RuntimeFactExtraction> {
|
|
46
|
+
const nodes: WorldNode[] = [];
|
|
47
|
+
const diagnostics: ExtractionDiagnostic[] = [];
|
|
48
|
+
const visit = async (directory: string): Promise<void> => {
|
|
49
|
+
let entries: Dirent[];
|
|
50
|
+
try {
|
|
51
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const relativePath = path.relative(this.deps.rootDir, directory).split(path.sep).join("/");
|
|
54
|
+
diagnostics.push(
|
|
55
|
+
ExtractionDiagnostic.create({
|
|
56
|
+
code: "source-read-failure",
|
|
57
|
+
path: PathKey.create(relativePath),
|
|
58
|
+
payload: { message: error instanceof Error ? error.message : String(error) },
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
entries.sort((left, right) => compareStrings(left.name, right.name));
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const absolutePath = path.join(directory, entry.name);
|
|
66
|
+
const relativePath = path.relative(this.deps.rootDir, absolutePath).split(path.sep).join("/");
|
|
67
|
+
if (entry.isSymbolicLink()) {
|
|
68
|
+
diagnostics.push(
|
|
69
|
+
ExtractionDiagnostic.create({
|
|
70
|
+
code: "unsupported-file-type",
|
|
71
|
+
path: PathKey.create(relativePath),
|
|
72
|
+
payload: { fileType: "symlink", sourceKind: this.deps.sourceKind },
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
await visit(absolutePath);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!relativePath.endsWith(".ts") || !this.deps.include(relativePath)) continue;
|
|
82
|
+
const pathKey = PathKey.create(relativePath);
|
|
83
|
+
if (!entry.isFile()) {
|
|
84
|
+
diagnostics.push(
|
|
85
|
+
ExtractionDiagnostic.create({
|
|
86
|
+
code: "unsupported-file-type",
|
|
87
|
+
path: pathKey,
|
|
88
|
+
payload: { fileType: "non-regular", sourceKind: this.deps.sourceKind },
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
let bytes: Uint8Array;
|
|
94
|
+
try {
|
|
95
|
+
bytes = await readFile(absolutePath);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
diagnostics.push(
|
|
98
|
+
ExtractionDiagnostic.create({
|
|
99
|
+
code: "source-read-failure",
|
|
100
|
+
path: pathKey,
|
|
101
|
+
payload: { message: error instanceof Error ? error.message : String(error) },
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const normalized = this.normalizer.normalize(bytes);
|
|
107
|
+
if (!normalized.ok) {
|
|
108
|
+
diagnostics.push(
|
|
109
|
+
ExtractionDiagnostic.create({
|
|
110
|
+
code: normalized.diagnostic.code,
|
|
111
|
+
path: pathKey,
|
|
112
|
+
payload: normalized.diagnostic.payload,
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const metadata = this.parseMetadata(normalized.text, pathKey, diagnostics);
|
|
118
|
+
nodes.push(
|
|
119
|
+
WorldNode.sourceFile({
|
|
120
|
+
path: pathKey,
|
|
121
|
+
digest: this.deps.hashingPort.sha256(normalized.bytes),
|
|
122
|
+
attributes: {
|
|
123
|
+
layer: metadata.layer,
|
|
124
|
+
sourceKind: this.deps.sourceKind,
|
|
125
|
+
unit: metadata.unit,
|
|
126
|
+
workItemIds: metadata.workItemIds,
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
await visit(path.join(this.deps.rootDir, this.deps.sourceRoot ?? "scripts/harness"));
|
|
133
|
+
const collisionPaths = new Set<string>();
|
|
134
|
+
const pathsByFolded = new Map<string, string[]>();
|
|
135
|
+
for (const node of nodes) {
|
|
136
|
+
const sourcePath = node.projection.type === "source-file" ? node.projection.pathKey : "";
|
|
137
|
+
const current = pathsByFolded.get(sourcePath.toLowerCase()) ?? [];
|
|
138
|
+
current.push(sourcePath);
|
|
139
|
+
pathsByFolded.set(sourcePath.toLowerCase(), current);
|
|
140
|
+
}
|
|
141
|
+
for (const paths of pathsByFolded.values()) {
|
|
142
|
+
const uniquePaths = [...new Set(paths)];
|
|
143
|
+
if (uniquePaths.length < 2) continue;
|
|
144
|
+
const candidatePaths = uniquePaths.sort(compareStrings);
|
|
145
|
+
for (const candidatePath of candidatePaths) collisionPaths.add(candidatePath);
|
|
146
|
+
diagnostics.push(
|
|
147
|
+
ExtractionDiagnostic.create({
|
|
148
|
+
code: "case-fold-path-collision",
|
|
149
|
+
path: PathKey.create(candidatePaths[0]),
|
|
150
|
+
payload: { candidatePaths },
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
nodes: nodes
|
|
156
|
+
.filter((node) => node.projection.type !== "source-file" || !collisionPaths.has(node.projection.pathKey))
|
|
157
|
+
.sort((left, right) => compareStrings(left.id.toString(), right.id.toString())),
|
|
158
|
+
edges: [],
|
|
159
|
+
diagnostics: diagnostics.sort((left, right) => {
|
|
160
|
+
const code = compareStrings(left.code, right.code);
|
|
161
|
+
if (code !== 0) return code;
|
|
162
|
+
return compareStrings(left.path?.toString() ?? "", right.path?.toString() ?? "");
|
|
163
|
+
}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private parseMetadata(
|
|
168
|
+
content: string,
|
|
169
|
+
pathKey: PathKey,
|
|
170
|
+
diagnostics: ExtractionDiagnostic[],
|
|
171
|
+
): { readonly unit: string | null; readonly layer: string | null; readonly workItemIds: readonly string[] } {
|
|
172
|
+
const units = new Set<string>();
|
|
173
|
+
const layers = new Set<string>();
|
|
174
|
+
const workItemIds = new Set<string>();
|
|
175
|
+
const lines = content.split("\n");
|
|
176
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
177
|
+
const line = lines[index];
|
|
178
|
+
const trimmed = line.trim();
|
|
179
|
+
if (
|
|
180
|
+
trimmed.length > 0 &&
|
|
181
|
+
!trimmed.startsWith("//") &&
|
|
182
|
+
!trimmed.startsWith("/*") &&
|
|
183
|
+
!trimmed.startsWith("*") &&
|
|
184
|
+
trimmed !== "*/"
|
|
185
|
+
) {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
const unit = UNIT_TAG.exec(line);
|
|
189
|
+
if (unit) for (const value of splitValues(unit[1])) units.add(value);
|
|
190
|
+
const layer = LAYER_TAG.exec(line);
|
|
191
|
+
if (layer) for (const value of splitValues(layer[1])) layers.add(value);
|
|
192
|
+
const workItems = WORK_ITEM_TAG.exec(line);
|
|
193
|
+
if (workItems) {
|
|
194
|
+
const matches = workItems[1].match(/\bWI-\d+\b/g) ?? [];
|
|
195
|
+
if (matches.length === 0) {
|
|
196
|
+
diagnostics.push(
|
|
197
|
+
ExtractionDiagnostic.create({
|
|
198
|
+
code: "malformed-source-work-item",
|
|
199
|
+
path: pathKey,
|
|
200
|
+
line: index + 1,
|
|
201
|
+
payload: { raw: workItems[1] },
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
for (const workItemId of matches) workItemIds.add(workItemId);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (units.size === 0) {
|
|
209
|
+
diagnostics.push(ExtractionDiagnostic.create({ code: "missing-source-unit", path: pathKey }));
|
|
210
|
+
} else if (units.size > 1) {
|
|
211
|
+
diagnostics.push(
|
|
212
|
+
ExtractionDiagnostic.create({
|
|
213
|
+
code: "ambiguous-source-unit",
|
|
214
|
+
path: pathKey,
|
|
215
|
+
payload: { candidates: [...units].sort(compareStrings) },
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (layers.size === 0) {
|
|
220
|
+
diagnostics.push(ExtractionDiagnostic.create({ code: "missing-source-layer", path: pathKey }));
|
|
221
|
+
} else if (layers.size > 1) {
|
|
222
|
+
diagnostics.push(
|
|
223
|
+
ExtractionDiagnostic.create({
|
|
224
|
+
code: "ambiguous-source-layer",
|
|
225
|
+
path: pathKey,
|
|
226
|
+
payload: { candidates: [...layers].sort(compareStrings) },
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
unit: units.size === 1 ? [...units][0] : null,
|
|
232
|
+
layer: layers.size === 1 ? [...layers][0] : null,
|
|
233
|
+
workItemIds: [...workItemIds].sort(compareStrings),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @unit world-model
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-289, WI-291
|
|
4
|
+
|
|
5
|
+
import { ExtractionDiagnostic } from "../../domain/entities/extraction-diagnostic.js";
|
|
6
|
+
import { CorpusRole } from "../../domain/value-objects/corpus-role.js";
|
|
7
|
+
import type {
|
|
8
|
+
DesignFactCandidateExtraction,
|
|
9
|
+
DesignFactSource,
|
|
10
|
+
TraceabilityDesignFactIndex,
|
|
11
|
+
} from "./design-fact-extraction.js";
|
|
12
|
+
import { FileSystemDesignFactScope, type MarkdownDesignFactExtractor } from "./markdown-design-fact-extractor.js";
|
|
13
|
+
|
|
14
|
+
export interface UnitFactExtractorDeps {
|
|
15
|
+
readonly rootDir: string;
|
|
16
|
+
readonly markdownExtractor: MarkdownDesignFactExtractor;
|
|
17
|
+
readonly unitRoot?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class UnitFactExtractor extends FileSystemDesignFactScope implements DesignFactSource {
|
|
21
|
+
constructor(deps: UnitFactExtractorDeps) {
|
|
22
|
+
super({
|
|
23
|
+
rootDir: deps.rootDir,
|
|
24
|
+
relativeRoot: deps.unitRoot ?? "docs/product/units",
|
|
25
|
+
role: CorpusRole.product(),
|
|
26
|
+
markdownExtractor: deps.markdownExtractor,
|
|
27
|
+
include: (relativePath) => /\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*_unit\.md$/.test(relativePath),
|
|
28
|
+
skip: (relativePath) => !/\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*_unit\.md$/.test(relativePath),
|
|
29
|
+
metadata: (relativePath, ownerIndex) => ({
|
|
30
|
+
unitId: ownerIndex.unitIdByDefinitionPath.get(relativePath),
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
override async extract(ownerIndex: TraceabilityDesignFactIndex): Promise<DesignFactCandidateExtraction> {
|
|
36
|
+
const extracted = await super.extract(ownerIndex);
|
|
37
|
+
const invalidPaths = new Map<string, "noncanonical-unit-definition-path" | "unknown-unit-definition-owner">();
|
|
38
|
+
for (const candidate of extracted.nodeCandidates) {
|
|
39
|
+
if (candidate.node.projection.type !== "artifact") continue;
|
|
40
|
+
const fileName = candidate.path.toString().split("/").at(-1) ?? "";
|
|
41
|
+
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*_unit\.md$/.test(fileName)) {
|
|
42
|
+
invalidPaths.set(candidate.path.toString(), "noncanonical-unit-definition-path");
|
|
43
|
+
} else if (candidate.node.attributes.unitId === undefined) {
|
|
44
|
+
invalidPaths.set(candidate.path.toString(), "unknown-unit-definition-owner");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const diagnostics = [...extracted.diagnostics];
|
|
48
|
+
for (const [invalidPath, code] of invalidPaths) {
|
|
49
|
+
const candidate = extracted.nodeCandidates.find((entry) => entry.path.toString() === invalidPath);
|
|
50
|
+
if (candidate) {
|
|
51
|
+
diagnostics.push(
|
|
52
|
+
ExtractionDiagnostic.create({
|
|
53
|
+
code,
|
|
54
|
+
path: candidate.path,
|
|
55
|
+
payload: { fileName: invalidPath.split("/").at(-1) ?? invalidPath },
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
nodeCandidates: extracted.nodeCandidates.filter((candidate) => !invalidPaths.has(candidate.path.toString())),
|
|
62
|
+
workItemReferences: extracted.workItemReferences.filter(
|
|
63
|
+
(reference) => !invalidPaths.has(reference.path.toString()),
|
|
64
|
+
),
|
|
65
|
+
reflectionReferences: extracted.reflectionReferences.filter(
|
|
66
|
+
(reference) => !invalidPaths.has(reference.path.toString()),
|
|
67
|
+
),
|
|
68
|
+
diagnostics,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
package/scripts/harness/world-model/infrastructure/adapters/world-control-declaration-mapper.ts
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// @unit world-model
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-294
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
ConstraintDeclarationSet,
|
|
7
|
+
WorldControlDiagnosticDto,
|
|
8
|
+
} from "../../application/ports/world-control-declaration-repository-port.js";
|
|
9
|
+
import {
|
|
10
|
+
type ConstraintFactType,
|
|
11
|
+
ConstraintRecord,
|
|
12
|
+
MalformedConstraintDeclaration,
|
|
13
|
+
} from "../../domain/entities/constraint-record.js";
|
|
14
|
+
import {
|
|
15
|
+
AdoptionBaseline,
|
|
16
|
+
AdoptionBaselineEntry,
|
|
17
|
+
SemanticDebtDeclaration,
|
|
18
|
+
WorldWaiver,
|
|
19
|
+
} from "../../domain/entities/control-declarations.js";
|
|
20
|
+
import { EvaluationId } from "../../domain/value-objects/evaluation-id.js";
|
|
21
|
+
import { ExplicitConstraintRelation } from "../../domain/value-objects/explicit-constraint-relation.js";
|
|
22
|
+
import { ExplicitNodeAlias } from "../../domain/value-objects/explicit-node-alias.js";
|
|
23
|
+
import { NodePin } from "../../domain/value-objects/node-pin.js";
|
|
24
|
+
import { Sha256Digest } from "../../domain/value-objects/sha256-digest.js";
|
|
25
|
+
import { ViolationFingerprint } from "../../domain/value-objects/violation-fingerprint.js";
|
|
26
|
+
import { WcrRuleId } from "../../domain/value-objects/wcr-rule-id.js";
|
|
27
|
+
import { WorldNodeId } from "../../domain/value-objects/world-node-id.js";
|
|
28
|
+
|
|
29
|
+
const CONSTRAINT_SCHEMA = "phasegate-world-constraints/v1" as const;
|
|
30
|
+
const BASELINE_SCHEMA = "phasegate-world-adoption-baseline/v1";
|
|
31
|
+
const WAIVER_SCHEMA = "phasegate-world-waivers/v1";
|
|
32
|
+
const DEBT_SCHEMA = "phasegate-world-debts/v1";
|
|
33
|
+
const FACT_TYPES: readonly ConstraintFactType[] = ["references", "depends-on", "refines", "content-equals"];
|
|
34
|
+
const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
|
|
35
|
+
|
|
36
|
+
export class InvalidControlDeclarationError extends Error {
|
|
37
|
+
constructor(message: string) {
|
|
38
|
+
super(`Invalid World control declaration: ${message}`);
|
|
39
|
+
this.name = "InvalidControlDeclarationError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const objectAt = (value: unknown, field: string): Record<string, unknown> => {
|
|
44
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
45
|
+
throw new InvalidControlDeclarationError(`${field} must be an object`);
|
|
46
|
+
}
|
|
47
|
+
return value as Record<string, unknown>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const arrayAt = (value: unknown, field: string): readonly unknown[] => {
|
|
51
|
+
if (!Array.isArray(value)) throw new InvalidControlDeclarationError(`${field} must be an array`);
|
|
52
|
+
return value;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const stringAt = (value: unknown, field: string): string => {
|
|
56
|
+
if (typeof value !== "string") throw new InvalidControlDeclarationError(`${field} must be a string`);
|
|
57
|
+
return value;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const nullableStringAt = (value: unknown, field: string): string | null => {
|
|
61
|
+
if (value === null) return null;
|
|
62
|
+
return stringAt(value, field);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const exactKeys = (record: Record<string, unknown>, keys: readonly string[], field: string): void => {
|
|
66
|
+
const actual = Object.keys(record).sort(compareStrings);
|
|
67
|
+
const expected = [...keys].sort(compareStrings);
|
|
68
|
+
const missing = expected.filter((key) => !(key in record));
|
|
69
|
+
const unsupported = actual.filter((key) => !expected.includes(key));
|
|
70
|
+
if (missing.length > 0 || unsupported.length > 0) {
|
|
71
|
+
throw new InvalidControlDeclarationError(
|
|
72
|
+
`${field} fields are invalid (missing=${missing.join(",") || "none"}; unsupported=${unsupported.join(",") || "none"})`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const withField = <T>(field: string, operation: () => T): T => {
|
|
78
|
+
try {
|
|
79
|
+
return operation();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error instanceof InvalidControlDeclarationError) throw error;
|
|
82
|
+
throw new InvalidControlDeclarationError(`${field}: ${error instanceof Error ? error.message : String(error)}`);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const parsePin = (value: unknown, field: string): NodePin => {
|
|
87
|
+
const record = objectAt(value, field);
|
|
88
|
+
exactKeys(record, ["nodeId", "contentDigest"], field);
|
|
89
|
+
return NodePin.create({
|
|
90
|
+
nodeId: withField(`${field}.nodeId`, () => WorldNodeId.parse(stringAt(record.nodeId, `${field}.nodeId`))),
|
|
91
|
+
contentDigest: withField(`${field}.contentDigest`, () =>
|
|
92
|
+
Sha256Digest.create(stringAt(record.contentDigest, `${field}.contentDigest`)),
|
|
93
|
+
),
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const malformed = (
|
|
98
|
+
artifactId: WorldNodeId,
|
|
99
|
+
locator: string,
|
|
100
|
+
declaredConstraintId: string | undefined,
|
|
101
|
+
reason: string,
|
|
102
|
+
): MalformedConstraintDeclaration =>
|
|
103
|
+
MalformedConstraintDeclaration.create({
|
|
104
|
+
declaredConstraintId,
|
|
105
|
+
declarationArtifactId: artifactId,
|
|
106
|
+
declarationLocator: locator,
|
|
107
|
+
reasons: [reason],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const parseConstraint = (value: unknown, index: number, artifactId: WorldNodeId): ConstraintRecord => {
|
|
111
|
+
const field = `/constraints/${index}`;
|
|
112
|
+
const record = objectAt(value, field);
|
|
113
|
+
exactKeys(record, ["constraintId", "factType", "claimant", "premise", "applicableRuleIds"], field);
|
|
114
|
+
const factType = stringAt(record.factType, `${field}/factType`);
|
|
115
|
+
if (!FACT_TYPES.includes(factType as ConstraintFactType)) {
|
|
116
|
+
throw new InvalidControlDeclarationError(`${field}/factType is unsupported`);
|
|
117
|
+
}
|
|
118
|
+
return ConstraintRecord.create({
|
|
119
|
+
constraintId: withField(`${field}/constraintId`, () =>
|
|
120
|
+
WorldNodeId.parse(stringAt(record.constraintId, `${field}/constraintId`)),
|
|
121
|
+
),
|
|
122
|
+
schemaVersion: CONSTRAINT_SCHEMA,
|
|
123
|
+
factType: factType as ConstraintFactType,
|
|
124
|
+
claimant: parsePin(record.claimant, `${field}.claimant`),
|
|
125
|
+
premise: parsePin(record.premise, `${field}.premise`),
|
|
126
|
+
applicableRuleIds: arrayAt(record.applicableRuleIds, `${field}/applicableRuleIds`).map((ruleId, ruleIndex) => {
|
|
127
|
+
const parsed = withField(`${field}/applicableRuleIds/${ruleIndex}`, () =>
|
|
128
|
+
WcrRuleId.create(stringAt(ruleId, "ruleId")),
|
|
129
|
+
);
|
|
130
|
+
if (parsed.toString() === "WCR-001") {
|
|
131
|
+
throw new InvalidControlDeclarationError(`${field}/applicableRuleIds cannot contain admission rule WCR-001`);
|
|
132
|
+
}
|
|
133
|
+
return parsed;
|
|
134
|
+
}),
|
|
135
|
+
declarationArtifactId: artifactId,
|
|
136
|
+
declarationLocator: field,
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const duplicateValues = (values: readonly (string | null)[]): ReadonlySet<string> => {
|
|
141
|
+
const counts = new Map<string, number>();
|
|
142
|
+
for (const value of values) {
|
|
143
|
+
if (value !== null) counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
144
|
+
}
|
|
145
|
+
return new Set([...counts.entries()].filter(([, count]) => count > 1).map(([value]) => value));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const diagnostic = (code: string, locator: string | null, message: string): WorldControlDiagnosticDto =>
|
|
149
|
+
Object.freeze({ code, path: "phasegate.world-constraints.json", locator, message });
|
|
150
|
+
|
|
151
|
+
export function mapConstraintDeclarationDocument(
|
|
152
|
+
value: unknown,
|
|
153
|
+
declarationArtifactId: WorldNodeId,
|
|
154
|
+
): ConstraintDeclarationSet {
|
|
155
|
+
const document = objectAt(value, "document");
|
|
156
|
+
exactKeys(document, ["schemaVersion", "constraints", "aliases"], "document");
|
|
157
|
+
if (stringAt(document.schemaVersion, "schemaVersion") !== CONSTRAINT_SCHEMA) {
|
|
158
|
+
throw new InvalidControlDeclarationError("unsupported constraints schemaVersion");
|
|
159
|
+
}
|
|
160
|
+
const rawConstraints = arrayAt(document.constraints, "constraints");
|
|
161
|
+
const candidateIds = rawConstraints.map((candidate) => {
|
|
162
|
+
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return null;
|
|
163
|
+
return typeof (candidate as Record<string, unknown>).constraintId === "string"
|
|
164
|
+
? ((candidate as Record<string, unknown>).constraintId as string)
|
|
165
|
+
: null;
|
|
166
|
+
});
|
|
167
|
+
const duplicateConstraintIds = duplicateValues(candidateIds);
|
|
168
|
+
const records: ConstraintRecord[] = [];
|
|
169
|
+
const malformedDeclarations: MalformedConstraintDeclaration[] = [];
|
|
170
|
+
for (const [index, candidate] of rawConstraints.entries()) {
|
|
171
|
+
const locator = `/constraints/${index}`;
|
|
172
|
+
const declaredConstraintId = candidateIds[index] ?? undefined;
|
|
173
|
+
if (declaredConstraintId !== undefined && duplicateConstraintIds.has(declaredConstraintId)) {
|
|
174
|
+
malformedDeclarations.push(
|
|
175
|
+
malformed(
|
|
176
|
+
declarationArtifactId,
|
|
177
|
+
locator,
|
|
178
|
+
declaredConstraintId,
|
|
179
|
+
`duplicate constraintId ${declaredConstraintId}`,
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
records.push(parseConstraint(candidate, index, declarationArtifactId));
|
|
186
|
+
} catch (error) {
|
|
187
|
+
malformedDeclarations.push(
|
|
188
|
+
malformed(
|
|
189
|
+
declarationArtifactId,
|
|
190
|
+
locator,
|
|
191
|
+
declaredConstraintId,
|
|
192
|
+
error instanceof Error ? error.message : String(error),
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const rawAliases = arrayAt(document.aliases, "aliases");
|
|
199
|
+
const aliasSources = rawAliases.map((candidate) => {
|
|
200
|
+
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return null;
|
|
201
|
+
return typeof (candidate as Record<string, unknown>).from === "string"
|
|
202
|
+
? ((candidate as Record<string, unknown>).from as string)
|
|
203
|
+
: null;
|
|
204
|
+
});
|
|
205
|
+
const duplicateAliasSources = duplicateValues(aliasSources);
|
|
206
|
+
const aliases: ExplicitNodeAlias[] = [];
|
|
207
|
+
const diagnostics: WorldControlDiagnosticDto[] = [];
|
|
208
|
+
for (const [index, candidate] of rawAliases.entries()) {
|
|
209
|
+
const locator = `/aliases/${index}`;
|
|
210
|
+
const source = aliasSources[index];
|
|
211
|
+
if (source !== null && duplicateAliasSources.has(source)) continue;
|
|
212
|
+
try {
|
|
213
|
+
const record = objectAt(candidate, locator);
|
|
214
|
+
exactKeys(record, ["from", "to"], locator);
|
|
215
|
+
aliases.push(
|
|
216
|
+
ExplicitNodeAlias.create({
|
|
217
|
+
from: WorldNodeId.parse(stringAt(record.from, `${locator}/from`)),
|
|
218
|
+
to: WorldNodeId.parse(stringAt(record.to, `${locator}/to`)),
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
diagnostics.push(
|
|
223
|
+
diagnostic("malformed-alias-declaration", locator, error instanceof Error ? error.message : String(error)),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const source of [...duplicateAliasSources].sort(compareStrings)) {
|
|
228
|
+
diagnostics.push(diagnostic("duplicate-alias-source", null, `No winner for duplicate alias source ${source}`));
|
|
229
|
+
}
|
|
230
|
+
for (const constraintId of [...duplicateConstraintIds].sort(compareStrings)) {
|
|
231
|
+
diagnostics.push(
|
|
232
|
+
diagnostic("duplicate-constraint-id", null, `No winner for duplicate constraint ID ${constraintId}`),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
records.sort((left, right) => compareStrings(left.constraintId.toString(), right.constraintId.toString()));
|
|
237
|
+
aliases.sort((left, right) => compareStrings(left.from.toString(), right.from.toString()));
|
|
238
|
+
malformedDeclarations.sort((left, right) => compareStrings(left.declarationLocator, right.declarationLocator));
|
|
239
|
+
diagnostics.sort((left, right) => compareStrings(left.code, right.code));
|
|
240
|
+
const relations = records
|
|
241
|
+
.filter((record) => record.factType !== "content-equals")
|
|
242
|
+
.map((record) =>
|
|
243
|
+
ExplicitConstraintRelation.create({
|
|
244
|
+
constraintId: record.constraintId,
|
|
245
|
+
factType: record.factType,
|
|
246
|
+
claimantId: record.claimant.nodeId,
|
|
247
|
+
premiseId: record.premise.nodeId,
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
return Object.freeze({
|
|
251
|
+
schemaVersion: CONSTRAINT_SCHEMA,
|
|
252
|
+
records: Object.freeze(records),
|
|
253
|
+
malformedDeclarations: Object.freeze(malformedDeclarations),
|
|
254
|
+
aliases: Object.freeze(aliases),
|
|
255
|
+
relations: Object.freeze(relations),
|
|
256
|
+
diagnostics: Object.freeze(diagnostics),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function mapAdoptionBaselineDocument(value: unknown): AdoptionBaseline {
|
|
261
|
+
try {
|
|
262
|
+
const document = objectAt(value, "document");
|
|
263
|
+
exactKeys(
|
|
264
|
+
document,
|
|
265
|
+
[
|
|
266
|
+
"schemaVersion",
|
|
267
|
+
"rulesetVersion",
|
|
268
|
+
"sourceEvaluationId",
|
|
269
|
+
"sourceCorpusRoot",
|
|
270
|
+
"sourceConstraintRoot",
|
|
271
|
+
"adoptedByWorkItemId",
|
|
272
|
+
"adoptionReason",
|
|
273
|
+
"entries",
|
|
274
|
+
],
|
|
275
|
+
"document",
|
|
276
|
+
);
|
|
277
|
+
if (stringAt(document.schemaVersion, "schemaVersion") !== BASELINE_SCHEMA) {
|
|
278
|
+
throw new InvalidControlDeclarationError("unsupported adoption baseline schemaVersion");
|
|
279
|
+
}
|
|
280
|
+
const entries = arrayAt(document.entries, "entries").map((value, index) => {
|
|
281
|
+
const field = `/entries/${index}`;
|
|
282
|
+
const entry = objectAt(value, field);
|
|
283
|
+
exactKeys(entry, ["violationFingerprint", "ruleId", "constraintId"], field);
|
|
284
|
+
const constraintIdValue = nullableStringAt(entry.constraintId, `${field}/constraintId`);
|
|
285
|
+
return AdoptionBaselineEntry.create({
|
|
286
|
+
violationFingerprint: ViolationFingerprint.create(
|
|
287
|
+
stringAt(entry.violationFingerprint, `${field}/violationFingerprint`),
|
|
288
|
+
),
|
|
289
|
+
ruleId: WcrRuleId.create(stringAt(entry.ruleId, `${field}/ruleId`)),
|
|
290
|
+
constraintId: constraintIdValue === null ? null : WorldNodeId.parse(constraintIdValue),
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
return AdoptionBaseline.create({
|
|
294
|
+
schemaVersion: BASELINE_SCHEMA,
|
|
295
|
+
rulesetVersion: stringAt(document.rulesetVersion, "rulesetVersion"),
|
|
296
|
+
sourceEvaluationId: EvaluationId.parse(stringAt(document.sourceEvaluationId, "sourceEvaluationId")),
|
|
297
|
+
sourceCorpusRoot: Sha256Digest.create(stringAt(document.sourceCorpusRoot, "sourceCorpusRoot")),
|
|
298
|
+
sourceConstraintRoot: Sha256Digest.create(stringAt(document.sourceConstraintRoot, "sourceConstraintRoot")),
|
|
299
|
+
adoptedByWorkItemId: stringAt(document.adoptedByWorkItemId, "adoptedByWorkItemId"),
|
|
300
|
+
adoptionReason: stringAt(document.adoptionReason, "adoptionReason"),
|
|
301
|
+
entries,
|
|
302
|
+
});
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error instanceof InvalidControlDeclarationError) throw error;
|
|
305
|
+
throw new InvalidControlDeclarationError(error instanceof Error ? error.message : String(error));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function mapWaiverDocument(value: unknown): readonly WorldWaiver[] {
|
|
310
|
+
try {
|
|
311
|
+
const document = objectAt(value, "document");
|
|
312
|
+
exactKeys(document, ["schemaVersion", "waivers"], "document");
|
|
313
|
+
if (stringAt(document.schemaVersion, "schemaVersion") !== WAIVER_SCHEMA) {
|
|
314
|
+
throw new InvalidControlDeclarationError("unsupported waiver schemaVersion");
|
|
315
|
+
}
|
|
316
|
+
const waivers = arrayAt(document.waivers, "waivers").map((value, index) => {
|
|
317
|
+
const field = `/waivers/${index}`;
|
|
318
|
+
const waiver = objectAt(value, field);
|
|
319
|
+
exactKeys(waiver, ["waiverId", "violationFingerprint", "reason", "expiresOn", "workItemId", "renewalOf"], field);
|
|
320
|
+
return WorldWaiver.create({
|
|
321
|
+
schemaVersion: WAIVER_SCHEMA,
|
|
322
|
+
waiverId: stringAt(waiver.waiverId, `${field}/waiverId`),
|
|
323
|
+
violationFingerprint: ViolationFingerprint.create(
|
|
324
|
+
stringAt(waiver.violationFingerprint, `${field}/violationFingerprint`),
|
|
325
|
+
),
|
|
326
|
+
reason: stringAt(waiver.reason, `${field}/reason`),
|
|
327
|
+
expiresOn: stringAt(waiver.expiresOn, `${field}/expiresOn`),
|
|
328
|
+
workItemId: stringAt(waiver.workItemId, `${field}/workItemId`),
|
|
329
|
+
renewalOf: nullableStringAt(waiver.renewalOf, `${field}/renewalOf`),
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
const duplicateIds = duplicateValues(waivers.map((waiver) => waiver.waiverId));
|
|
333
|
+
const duplicateFingerprints = duplicateValues(waivers.map((waiver) => waiver.violationFingerprint.toString()));
|
|
334
|
+
if (duplicateIds.size > 0 || duplicateFingerprints.size > 0) {
|
|
335
|
+
throw new InvalidControlDeclarationError("waivers contain duplicate waiverId or violationFingerprint; no winner");
|
|
336
|
+
}
|
|
337
|
+
return Object.freeze(waivers.sort((left, right) => compareStrings(left.waiverId, right.waiverId)));
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (error instanceof InvalidControlDeclarationError) throw error;
|
|
340
|
+
throw new InvalidControlDeclarationError(error instanceof Error ? error.message : String(error));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function mapSemanticDebtDocument(value: unknown): readonly SemanticDebtDeclaration[] {
|
|
345
|
+
try {
|
|
346
|
+
const document = objectAt(value, "document");
|
|
347
|
+
exactKeys(document, ["schemaVersion", "debts"], "document");
|
|
348
|
+
if (stringAt(document.schemaVersion, "schemaVersion") !== DEBT_SCHEMA) {
|
|
349
|
+
throw new InvalidControlDeclarationError("unsupported semantic debt schemaVersion");
|
|
350
|
+
}
|
|
351
|
+
const debts = arrayAt(document.debts, "debts").map((value, index) => {
|
|
352
|
+
const field = `/debts/${index}`;
|
|
353
|
+
const debt = objectAt(value, field);
|
|
354
|
+
exactKeys(
|
|
355
|
+
debt,
|
|
356
|
+
["debtId", "kind", "title", "reason", "ownerUnit", "introducedByWorkItemId", "references"],
|
|
357
|
+
field,
|
|
358
|
+
);
|
|
359
|
+
const kind = stringAt(debt.kind, `${field}/kind`);
|
|
360
|
+
if (kind !== "semantic") throw new InvalidControlDeclarationError(`${field}/kind must be semantic`);
|
|
361
|
+
return SemanticDebtDeclaration.create({
|
|
362
|
+
schemaVersion: DEBT_SCHEMA,
|
|
363
|
+
debtId: stringAt(debt.debtId, `${field}/debtId`),
|
|
364
|
+
kind,
|
|
365
|
+
title: stringAt(debt.title, `${field}/title`),
|
|
366
|
+
reason: stringAt(debt.reason, `${field}/reason`),
|
|
367
|
+
ownerUnit: stringAt(debt.ownerUnit, `${field}/ownerUnit`),
|
|
368
|
+
introducedByWorkItemId: stringAt(debt.introducedByWorkItemId, `${field}/introducedByWorkItemId`),
|
|
369
|
+
references: arrayAt(debt.references, `${field}/references`).map((reference, referenceIndex) =>
|
|
370
|
+
WorldNodeId.parse(stringAt(reference, `${field}/references/${referenceIndex}`)),
|
|
371
|
+
),
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
const duplicateIds = duplicateValues(debts.map((debt) => debt.debtId));
|
|
375
|
+
if (duplicateIds.size > 0) {
|
|
376
|
+
throw new InvalidControlDeclarationError("semantic debts contain duplicate debtId; no winner");
|
|
377
|
+
}
|
|
378
|
+
return Object.freeze(debts.sort((left, right) => compareStrings(left.debtId, right.debtId)));
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (error instanceof InvalidControlDeclarationError) throw error;
|
|
381
|
+
throw new InvalidControlDeclarationError(error instanceof Error ? error.message : String(error));
|
|
382
|
+
}
|
|
383
|
+
}
|