phasegate 0.229.0 → 0.254.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 +51 -1
- package/README.ja.md +28 -1
- package/README.md +27 -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/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 +34 -0
- package/package.json +1 -1
- package/scripts/harness/attestation/application/ports/sha256-capability.ts +25 -0
- package/scripts/harness/attestation/composition-root.ts +14 -4
- package/scripts/harness/attestation/index.ts +6 -1
- 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/harness-api/domain/value-objects/known-harness-commands.ts +4 -1
- package/scripts/harness/main.ts +107 -1
- 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/traceability-world-read-dto.ts +67 -0
- package/scripts/harness/traceability-model/application/facades/traceability-world-read-facade.ts +372 -0
- package/scripts/harness/traceability-model/application/ports/traceability-world-read-source-port.ts +61 -0
- package/scripts/harness/traceability-model/composition-root.ts +21 -5
- package/scripts/harness/traceability-model/index.ts +17 -6
- package/scripts/harness/traceability-model/infrastructure/adapters/file-system-traceability-world-read-adapter.ts +223 -0
- package/scripts/harness/traceability-model/infrastructure/parsers/story-catalog-parser.ts +86 -8
- 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 +62 -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 +144 -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 +218 -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 +22 -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 +288 -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 +330 -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 +105 -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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// @unit world-model
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-291
|
|
4
|
+
|
|
5
|
+
import type { Edge } from "../../domain/entities/edge.js";
|
|
6
|
+
import { ExtractionDiagnostic } from "../../domain/entities/extraction-diagnostic.js";
|
|
7
|
+
import type { Snapshot } from "../../domain/entities/snapshot.js";
|
|
8
|
+
import type { WorldNode } from "../../domain/entities/world-node.js";
|
|
9
|
+
import type { CanonicalJsonSerializer } from "../../domain/services/canonical-json-serializer.js";
|
|
10
|
+
import type { SnapshotRootDeriver } from "../../domain/services/snapshot-root-deriver.js";
|
|
11
|
+
import type { Sha256Digest } from "../../domain/value-objects/sha256-digest.js";
|
|
12
|
+
import type { WorldFactSourcePort } from "../ports/world-fact-source-port.js";
|
|
13
|
+
|
|
14
|
+
export interface BuildSnapshotContract {
|
|
15
|
+
execute(): Promise<Snapshot>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BuildSnapshotUseCaseDeps {
|
|
19
|
+
readonly factSource: WorldFactSourcePort;
|
|
20
|
+
readonly rootDeriver: SnapshotRootDeriver;
|
|
21
|
+
readonly serializer: CanonicalJsonSerializer;
|
|
22
|
+
readonly schemaVersion: string;
|
|
23
|
+
readonly extractorVersion: string;
|
|
24
|
+
readonly corpusConfigDigest: Sha256Digest;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
|
|
28
|
+
|
|
29
|
+
export class BuildSnapshotUseCase implements BuildSnapshotContract {
|
|
30
|
+
constructor(private readonly deps: BuildSnapshotUseCaseDeps) {}
|
|
31
|
+
|
|
32
|
+
async execute(): Promise<Snapshot> {
|
|
33
|
+
const batches = await this.deps.factSource.extract();
|
|
34
|
+
const diagnostics = batches.flatMap((batch) => batch.diagnostics);
|
|
35
|
+
const nodes = this.admitNodes(
|
|
36
|
+
batches.flatMap((batch) => batch.nodes),
|
|
37
|
+
diagnostics,
|
|
38
|
+
);
|
|
39
|
+
const edges = this.admitEdges(
|
|
40
|
+
batches.flatMap((batch) => batch.edges),
|
|
41
|
+
new Set(nodes.map((node) => node.id.toString())),
|
|
42
|
+
diagnostics,
|
|
43
|
+
);
|
|
44
|
+
return this.deps.rootDeriver.buildSnapshot({
|
|
45
|
+
schemaVersion: this.deps.schemaVersion,
|
|
46
|
+
extractorVersion: this.deps.extractorVersion,
|
|
47
|
+
corpusConfigDigest: this.deps.corpusConfigDigest,
|
|
48
|
+
nodes,
|
|
49
|
+
edges,
|
|
50
|
+
extractionDiagnostics: diagnostics,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private admitNodes(candidates: readonly WorldNode[], diagnostics: ExtractionDiagnostic[]): readonly WorldNode[] {
|
|
55
|
+
const grouped = new Map<string, WorldNode[]>();
|
|
56
|
+
for (const candidate of candidates) {
|
|
57
|
+
const key = candidate.id.toString();
|
|
58
|
+
grouped.set(key, [...(grouped.get(key) ?? []), candidate]);
|
|
59
|
+
}
|
|
60
|
+
const admitted: WorldNode[] = [];
|
|
61
|
+
for (const [nodeId, entries] of [...grouped.entries()].sort(([left], [right]) => compareStrings(left, right))) {
|
|
62
|
+
if (entries.length === 1) {
|
|
63
|
+
admitted.push(entries[0]);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
diagnostics.push(
|
|
67
|
+
ExtractionDiagnostic.create({
|
|
68
|
+
code: "duplicate-node-id",
|
|
69
|
+
nodeId: entries[0].id,
|
|
70
|
+
payload: {
|
|
71
|
+
candidates: entries.length,
|
|
72
|
+
candidateFacts: entries
|
|
73
|
+
.map((entry) => this.deps.serializer.stringify(entry.toCanonicalValue()))
|
|
74
|
+
.sort(compareStrings),
|
|
75
|
+
nodeId,
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return admitted;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private admitEdges(
|
|
84
|
+
candidates: readonly Edge[],
|
|
85
|
+
nodeIds: ReadonlySet<string>,
|
|
86
|
+
diagnostics: ExtractionDiagnostic[],
|
|
87
|
+
): readonly Edge[] {
|
|
88
|
+
const unique = new Map<string, Edge>();
|
|
89
|
+
for (const edge of candidates) {
|
|
90
|
+
const key = this.deps.serializer.stringify(edge.toCanonicalValue());
|
|
91
|
+
if (!unique.has(key)) unique.set(key, edge);
|
|
92
|
+
}
|
|
93
|
+
const admitted: Edge[] = [];
|
|
94
|
+
for (const [edgeKey, edge] of [...unique.entries()].sort(([left], [right]) => compareStrings(left, right))) {
|
|
95
|
+
const missingEndpoints = [edge.from.toString(), edge.to.toString()]
|
|
96
|
+
.filter((endpoint) => !nodeIds.has(endpoint))
|
|
97
|
+
.sort(compareStrings);
|
|
98
|
+
if (missingEndpoints.length === 0) {
|
|
99
|
+
admitted.push(edge);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
diagnostics.push(
|
|
103
|
+
ExtractionDiagnostic.create({
|
|
104
|
+
code: "missing-edge-endpoint",
|
|
105
|
+
payload: { edge: edgeKey, missingEndpoints },
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return admitted;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// @unit world-model
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-295, WI-296
|
|
4
|
+
|
|
5
|
+
import type { CanonicalJsonSerializer } from "../../domain/services/canonical-json-serializer.js";
|
|
6
|
+
import type { ConstraintFindingDto } from "../../domain/services/constraint-evaluator.js";
|
|
7
|
+
import type { ObligationDerivationService } from "../../domain/services/obligation-derivation-service.js";
|
|
8
|
+
import type {
|
|
9
|
+
PolicyInputsDigestDerivation,
|
|
10
|
+
PolicyInputsDigestDeriver,
|
|
11
|
+
} from "../../domain/services/policy-inputs-digest-deriver.js";
|
|
12
|
+
import type { SnapshotRootDeriver } from "../../domain/services/snapshot-root-deriver.js";
|
|
13
|
+
import type { Sha256Digest } from "../../domain/value-objects/sha256-digest.js";
|
|
14
|
+
import type { WorldObligationReportDto } from "../dto/world-obligation-report-dto.js";
|
|
15
|
+
import type { ObligationReportWriterPort } from "../ports/obligation-report-writer-port.js";
|
|
16
|
+
import type {
|
|
17
|
+
AdoptionBaselineRepositoryPort,
|
|
18
|
+
SemanticDebtRepositoryPort,
|
|
19
|
+
WaiverDeclarationRepositoryPort,
|
|
20
|
+
WorldControlDiagnosticDto,
|
|
21
|
+
WorldControlReadResult,
|
|
22
|
+
} from "../ports/world-control-declaration-repository-port.js";
|
|
23
|
+
|
|
24
|
+
export interface DeriveObligationsInput {
|
|
25
|
+
readonly rulesetVersion: string;
|
|
26
|
+
readonly findings: readonly ConstraintFindingDto[];
|
|
27
|
+
readonly corpusRoot: Sha256Digest;
|
|
28
|
+
readonly constraintRoot: Sha256Digest;
|
|
29
|
+
readonly evaluationConfigDigest: Sha256Digest;
|
|
30
|
+
readonly policyAsOfDate: string | null;
|
|
31
|
+
readonly writeReport: boolean;
|
|
32
|
+
readonly reportPath?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type ReportPersistenceResult =
|
|
36
|
+
| { readonly state: "not-requested" }
|
|
37
|
+
| { readonly state: "written" }
|
|
38
|
+
| { readonly state: "failed"; readonly message: string };
|
|
39
|
+
|
|
40
|
+
export type DeriveObligationsResult =
|
|
41
|
+
| {
|
|
42
|
+
readonly status: "derived";
|
|
43
|
+
readonly report: WorldObligationReportDto;
|
|
44
|
+
readonly canonicalBytes: Uint8Array;
|
|
45
|
+
readonly persistence: ReportPersistenceResult;
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
readonly status: "invalid-policy-input";
|
|
49
|
+
readonly diagnostics: readonly WorldControlDiagnosticDto[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
interface Dependencies {
|
|
53
|
+
readonly baselineRepository: AdoptionBaselineRepositoryPort;
|
|
54
|
+
readonly waiverRepository: WaiverDeclarationRepositoryPort;
|
|
55
|
+
readonly semanticDebtRepository: SemanticDebtRepositoryPort;
|
|
56
|
+
readonly policyInputsDigestDeriver: PolicyInputsDigestDeriver;
|
|
57
|
+
readonly evaluationIdDeriver: SnapshotRootDeriver;
|
|
58
|
+
readonly obligationDerivationService: ObligationDerivationService;
|
|
59
|
+
readonly serializer: CanonicalJsonSerializer;
|
|
60
|
+
readonly writer: ObligationReportWriterPort;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
|
|
64
|
+
|
|
65
|
+
const diagnosticsOf = <T>(result: WorldControlReadResult<T>): readonly WorldControlDiagnosticDto[] =>
|
|
66
|
+
result.state === "invalid" ? result.diagnostics : [];
|
|
67
|
+
|
|
68
|
+
export class DeriveObligationsUseCase {
|
|
69
|
+
constructor(private readonly dependencies: Dependencies) {}
|
|
70
|
+
|
|
71
|
+
async execute(input: DeriveObligationsInput): Promise<DeriveObligationsResult> {
|
|
72
|
+
const [baselineResult, waiverResult, debtResult] = await Promise.all([
|
|
73
|
+
this.dependencies.baselineRepository.load(),
|
|
74
|
+
this.dependencies.waiverRepository.load(),
|
|
75
|
+
this.dependencies.semanticDebtRepository.load(),
|
|
76
|
+
]);
|
|
77
|
+
const diagnostics = [
|
|
78
|
+
...diagnosticsOf(baselineResult),
|
|
79
|
+
...diagnosticsOf(waiverResult),
|
|
80
|
+
...diagnosticsOf(debtResult),
|
|
81
|
+
].sort(
|
|
82
|
+
(left, right) =>
|
|
83
|
+
compareStrings(left.code, right.code) ||
|
|
84
|
+
compareStrings(left.path, right.path) ||
|
|
85
|
+
compareStrings(left.locator ?? "", right.locator ?? ""),
|
|
86
|
+
);
|
|
87
|
+
if (baselineResult.state === "invalid" || waiverResult.state === "invalid" || debtResult.state === "invalid") {
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
status: "invalid-policy-input",
|
|
90
|
+
diagnostics: Object.freeze(diagnostics),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let policy: PolicyInputsDigestDerivation;
|
|
95
|
+
try {
|
|
96
|
+
policy = this.dependencies.policyInputsDigestDeriver.derive({
|
|
97
|
+
baseline: baselineResult.value,
|
|
98
|
+
waivers: waiverResult.value,
|
|
99
|
+
semanticDebts: debtResult.value,
|
|
100
|
+
policyAsOfDate: input.policyAsOfDate,
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return Object.freeze({
|
|
104
|
+
status: "invalid-policy-input",
|
|
105
|
+
diagnostics: Object.freeze([
|
|
106
|
+
{
|
|
107
|
+
code: "invalid-policy-as-of-date",
|
|
108
|
+
path: "world.policyAsOfDate",
|
|
109
|
+
locator: null,
|
|
110
|
+
message: error instanceof Error ? error.message : String(error),
|
|
111
|
+
},
|
|
112
|
+
]),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const evaluation = this.dependencies.evaluationIdDeriver.deriveEvaluationId({
|
|
116
|
+
schemaVersion: "phasegate-world-evaluation/v1",
|
|
117
|
+
rulesetVersion: input.rulesetVersion,
|
|
118
|
+
corpusRoot: input.corpusRoot,
|
|
119
|
+
constraintRoot: input.constraintRoot,
|
|
120
|
+
evaluationConfigDigest: input.evaluationConfigDigest,
|
|
121
|
+
policyInputsDigest: policy.digest,
|
|
122
|
+
});
|
|
123
|
+
const report: WorldObligationReportDto = this.dependencies.obligationDerivationService.derive({
|
|
124
|
+
evaluationId: evaluation.evaluationId,
|
|
125
|
+
rulesetVersion: input.rulesetVersion,
|
|
126
|
+
policyInputsDigest: policy.digest,
|
|
127
|
+
findings: input.findings,
|
|
128
|
+
baseline: baselineResult.value,
|
|
129
|
+
waivers: waiverResult.value,
|
|
130
|
+
semanticDebts: debtResult.value,
|
|
131
|
+
policyAsOfDate: policy.resolvedPolicyAsOfDate,
|
|
132
|
+
});
|
|
133
|
+
const canonicalBytes = this.dependencies.serializer.serialize(report);
|
|
134
|
+
let persistence: ReportPersistenceResult = Object.freeze({
|
|
135
|
+
state: "not-requested",
|
|
136
|
+
});
|
|
137
|
+
if (input.writeReport) {
|
|
138
|
+
try {
|
|
139
|
+
await this.dependencies.writer.write(canonicalBytes, input.reportPath);
|
|
140
|
+
persistence = Object.freeze({ state: "written" });
|
|
141
|
+
} catch (error) {
|
|
142
|
+
persistence = Object.freeze({
|
|
143
|
+
state: "failed",
|
|
144
|
+
message: error instanceof Error ? error.message : String(error),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return Object.freeze({
|
|
149
|
+
status: "derived",
|
|
150
|
+
report,
|
|
151
|
+
canonicalBytes,
|
|
152
|
+
persistence,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// @unit world-model
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-296, WI-297
|
|
4
|
+
|
|
5
|
+
import { ExtractionDiagnostic } from "../../domain/entities/extraction-diagnostic.js";
|
|
6
|
+
import type { Snapshot } from "../../domain/entities/snapshot.js";
|
|
7
|
+
import { ConstraintEvaluator, type ConstraintFindingDto } from "../../domain/services/constraint-evaluator.js";
|
|
8
|
+
import type { SnapshotRootDeriver } from "../../domain/services/snapshot-root-deriver.js";
|
|
9
|
+
import { ChangeProvenance } from "../../domain/value-objects/change-provenance.js";
|
|
10
|
+
import { PathKey } from "../../domain/value-objects/path-key.js";
|
|
11
|
+
import type { Sha256Digest } from "../../domain/value-objects/sha256-digest.js";
|
|
12
|
+
import type {
|
|
13
|
+
ConstraintDeclarationRepositoryPort,
|
|
14
|
+
WorldControlDiagnosticDto,
|
|
15
|
+
} from "../ports/world-control-declaration-repository-port.js";
|
|
16
|
+
import type { BuildSnapshotContract } from "./build-snapshot-use-case.js";
|
|
17
|
+
import type { DeriveObligationsResult, DeriveObligationsUseCase } from "./derive-obligations-use-case.js";
|
|
18
|
+
|
|
19
|
+
export interface PolicyDatePort {
|
|
20
|
+
currentUtcDate(): string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type DeriveWorldObligationsResult =
|
|
24
|
+
| { readonly status: "derived"; readonly result: Extract<DeriveObligationsResult, { status: "derived" }> }
|
|
25
|
+
| { readonly status: "execution-failure"; readonly diagnostics: readonly WorldControlDiagnosticDto[] };
|
|
26
|
+
|
|
27
|
+
export interface DeriveWorldObligationsInput {
|
|
28
|
+
readonly writeReport: boolean;
|
|
29
|
+
readonly reportPath?: string;
|
|
30
|
+
readonly baselineSnapshot?: Snapshot;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Dependencies {
|
|
34
|
+
readonly buildSnapshot: BuildSnapshotContract;
|
|
35
|
+
readonly constraintRepository: ConstraintDeclarationRepositoryPort;
|
|
36
|
+
readonly rootDeriver: SnapshotRootDeriver;
|
|
37
|
+
readonly deriveObligations: DeriveObligationsUseCase;
|
|
38
|
+
readonly policyDate: PolicyDatePort;
|
|
39
|
+
readonly constraintConfigDigest: Sha256Digest;
|
|
40
|
+
readonly evaluationConfigDigest: Sha256Digest;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const RULESET_VERSION = "phasegate-world-wcr/v1";
|
|
44
|
+
|
|
45
|
+
export class DeriveWorldObligationsUseCase {
|
|
46
|
+
constructor(private readonly dependencies: Dependencies) {}
|
|
47
|
+
|
|
48
|
+
async execute(input: DeriveWorldObligationsInput): Promise<DeriveWorldObligationsResult> {
|
|
49
|
+
const [snapshot, constraints] = await Promise.all([
|
|
50
|
+
this.dependencies.buildSnapshot.execute(),
|
|
51
|
+
this.dependencies.constraintRepository.load(),
|
|
52
|
+
]);
|
|
53
|
+
if (constraints.state === "invalid") {
|
|
54
|
+
return { status: "execution-failure", diagnostics: constraints.diagnostics };
|
|
55
|
+
}
|
|
56
|
+
const declarationDiagnostics = [
|
|
57
|
+
...constraints.value.diagnostics.map((item) =>
|
|
58
|
+
ExtractionDiagnostic.create({
|
|
59
|
+
code: item.code,
|
|
60
|
+
path: PathKey.create(item.path),
|
|
61
|
+
payload: { locator: item.locator, message: item.message },
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
...constraints.value.malformedDeclarations.map((declaration) =>
|
|
65
|
+
ExtractionDiagnostic.create({
|
|
66
|
+
code: "malformed-constraint-declaration",
|
|
67
|
+
path: PathKey.create("phasegate.world-constraints.json"),
|
|
68
|
+
payload: declaration.toCanonicalValue(),
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
71
|
+
];
|
|
72
|
+
const constraintRoot = this.dependencies.rootDeriver.deriveConstraintRoot({
|
|
73
|
+
schemaVersion: constraints.value.schemaVersion,
|
|
74
|
+
rulesetVersion: RULESET_VERSION,
|
|
75
|
+
constraintConfigDigest: this.dependencies.constraintConfigDigest,
|
|
76
|
+
constraints: constraints.value.records.map((record) => ({
|
|
77
|
+
id: record.constraintId,
|
|
78
|
+
value: record.toCanonicalValue(),
|
|
79
|
+
})),
|
|
80
|
+
explicitClaims: [],
|
|
81
|
+
aliases: constraints.value.aliases.map((alias) => ({
|
|
82
|
+
id: alias.from,
|
|
83
|
+
value: alias.toCanonicalValue(),
|
|
84
|
+
})),
|
|
85
|
+
declarationDiagnostics,
|
|
86
|
+
}).root;
|
|
87
|
+
const evaluation = new ConstraintEvaluator().evaluateFull({
|
|
88
|
+
currentSnapshot: snapshot,
|
|
89
|
+
baselineSnapshot: input.baselineSnapshot,
|
|
90
|
+
records: constraints.value.records,
|
|
91
|
+
malformedDeclarations: constraints.value.malformedDeclarations,
|
|
92
|
+
aliases: constraints.value.aliases,
|
|
93
|
+
relations: constraints.value.relations,
|
|
94
|
+
changeProvenance: ChangeProvenance.between(input.baselineSnapshot ?? null, snapshot),
|
|
95
|
+
});
|
|
96
|
+
const implicitFindings = snapshot.extractionDiagnostics.flatMap((diagnostic): readonly ConstraintFindingDto[] => {
|
|
97
|
+
if (diagnostic.code !== "duplicate-node-id" || diagnostic.nodeId === undefined) return [];
|
|
98
|
+
const facts = Array.isArray(diagnostic.payload.candidateFacts) ? diagnostic.payload.candidateFacts : [];
|
|
99
|
+
const digests = facts
|
|
100
|
+
.flatMap((fact) => {
|
|
101
|
+
if (typeof fact !== "string") return [];
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(fact) as { contentDigest?: unknown };
|
|
104
|
+
return typeof parsed.contentDigest === "string" ? [parsed.contentDigest] : [];
|
|
105
|
+
} catch {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
.sort();
|
|
110
|
+
return [
|
|
111
|
+
{
|
|
112
|
+
ruleId: "WCR-005",
|
|
113
|
+
constraintId: null,
|
|
114
|
+
factType: null,
|
|
115
|
+
endpoint: null,
|
|
116
|
+
claimant: null,
|
|
117
|
+
premise: null,
|
|
118
|
+
declarationArtifactId: "",
|
|
119
|
+
declarationLocator: "",
|
|
120
|
+
evidence: {
|
|
121
|
+
nodeId: diagnostic.nodeId.toString(),
|
|
122
|
+
candidateCount:
|
|
123
|
+
typeof diagnostic.payload.candidates === "number" ? diagnostic.payload.candidates : digests.length,
|
|
124
|
+
candidateContentDigests: digests,
|
|
125
|
+
resolution: "global-node-id",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
});
|
|
130
|
+
const result = await this.dependencies.deriveObligations.execute({
|
|
131
|
+
rulesetVersion: RULESET_VERSION,
|
|
132
|
+
findings: [...evaluation.findings, ...implicitFindings],
|
|
133
|
+
corpusRoot: snapshot.corpusRoot,
|
|
134
|
+
constraintRoot,
|
|
135
|
+
evaluationConfigDigest: this.dependencies.evaluationConfigDigest,
|
|
136
|
+
policyAsOfDate: this.dependencies.policyDate.currentUtcDate(),
|
|
137
|
+
writeReport: input.writeReport,
|
|
138
|
+
reportPath: input.reportPath,
|
|
139
|
+
});
|
|
140
|
+
return result.status === "derived"
|
|
141
|
+
? { status: "derived", result }
|
|
142
|
+
: { status: "execution-failure", diagnostics: result.diagnostics };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @unit world-model
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-291
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
WorldExtractionDiagnosticDto,
|
|
7
|
+
WorldInspectionDto,
|
|
8
|
+
WorldInventoryCountDto,
|
|
9
|
+
WorldJsonObject,
|
|
10
|
+
} from "../dto/world-inspection-dto.js";
|
|
11
|
+
import type { BuildSnapshotContract } from "./build-snapshot-use-case.js";
|
|
12
|
+
|
|
13
|
+
export interface InspectWorldContract {
|
|
14
|
+
execute(): Promise<WorldInspectionDto>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface InspectWorldUseCaseDeps {
|
|
18
|
+
readonly buildSnapshot: BuildSnapshotContract;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
|
|
22
|
+
|
|
23
|
+
const countValues = (values: readonly string[]): readonly WorldInventoryCountDto[] => {
|
|
24
|
+
const counts = new Map<string, number>();
|
|
25
|
+
for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
26
|
+
return [...counts.entries()]
|
|
27
|
+
.sort(([left], [right]) => compareStrings(left, right))
|
|
28
|
+
.map(([value, count]) => ({ value, count }));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export class InspectWorldUseCase implements InspectWorldContract {
|
|
32
|
+
constructor(private readonly deps: InspectWorldUseCaseDeps) {}
|
|
33
|
+
|
|
34
|
+
async execute(): Promise<WorldInspectionDto> {
|
|
35
|
+
const snapshot = await this.deps.buildSnapshot.execute();
|
|
36
|
+
const nodes = snapshot.nodes
|
|
37
|
+
.map((node) => node.toCanonicalValue() as WorldJsonObject)
|
|
38
|
+
.sort((left, right) => compareStrings(String(left.id), String(right.id)));
|
|
39
|
+
const edges = snapshot.edges
|
|
40
|
+
.map((edge) => edge.toCanonicalValue() as WorldJsonObject)
|
|
41
|
+
.sort((left, right) => compareStrings(JSON.stringify(left), JSON.stringify(right)));
|
|
42
|
+
const diagnostics = snapshot.extractionDiagnostics
|
|
43
|
+
.map((diagnostic) => diagnostic.toCanonicalValue() as WorldExtractionDiagnosticDto)
|
|
44
|
+
.sort((left, right) => compareStrings(JSON.stringify(left), JSON.stringify(right)));
|
|
45
|
+
const artifactNodes = snapshot.nodes.filter((node) => node.projection.type === "artifact");
|
|
46
|
+
const hardDiagnosticCount = snapshot.extractionDiagnostics.filter(
|
|
47
|
+
(diagnostic) => diagnostic.code !== "not-present",
|
|
48
|
+
).length;
|
|
49
|
+
return {
|
|
50
|
+
snapshotId: snapshot.id.toString(),
|
|
51
|
+
schemaVersion: snapshot.schemaVersion,
|
|
52
|
+
extractorVersion: snapshot.extractorVersion,
|
|
53
|
+
corpusRoot: snapshot.corpusRoot.toString(),
|
|
54
|
+
summary: {
|
|
55
|
+
nodeCount: nodes.length,
|
|
56
|
+
edgeCount: edges.length,
|
|
57
|
+
diagnosticCount: diagnostics.length,
|
|
58
|
+
hardDiagnosticCount,
|
|
59
|
+
},
|
|
60
|
+
inventory: {
|
|
61
|
+
nodeTypes: countValues(snapshot.nodes.map((node) => node.id.nodeType)),
|
|
62
|
+
corpusRoles: countValues(
|
|
63
|
+
artifactNodes.map((node) => (node.projection.type === "artifact" ? node.projection.corpusRole : "")),
|
|
64
|
+
),
|
|
65
|
+
artifactKinds: countValues(
|
|
66
|
+
artifactNodes.map((node) => (node.projection.type === "artifact" ? node.projection.artifactKind : "")),
|
|
67
|
+
),
|
|
68
|
+
},
|
|
69
|
+
nodes,
|
|
70
|
+
edges,
|
|
71
|
+
diagnostics,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// @unit world-model
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-296
|
|
4
|
+
|
|
5
|
+
import { ConstraintRecord } from "../../domain/entities/constraint-record.js";
|
|
6
|
+
import type { Snapshot } from "../../domain/entities/snapshot.js";
|
|
7
|
+
import { NodePin } from "../../domain/value-objects/node-pin.js";
|
|
8
|
+
import { WorldNodeId } from "../../domain/value-objects/world-node-id.js";
|
|
9
|
+
import type {
|
|
10
|
+
ConstraintDeclarationRepositoryPort,
|
|
11
|
+
ConstraintDeclarationSet,
|
|
12
|
+
WorldControlDiagnosticDto,
|
|
13
|
+
} from "../ports/world-control-declaration-repository-port.js";
|
|
14
|
+
import type { BuildSnapshotContract } from "./build-snapshot-use-case.js";
|
|
15
|
+
|
|
16
|
+
export type PinEndpointRole = "claimant" | "premise";
|
|
17
|
+
|
|
18
|
+
export interface PinConstraintEndpointInput {
|
|
19
|
+
readonly constraintId: string;
|
|
20
|
+
readonly endpoint: PinEndpointRole;
|
|
21
|
+
readonly apply: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type PinConstraintEndpointResult =
|
|
25
|
+
| { readonly status: "preview" | "applied" | "unchanged"; readonly candidate: PinCandidateDto }
|
|
26
|
+
| { readonly status: "domain-failure"; readonly code: string; readonly message: string }
|
|
27
|
+
| { readonly status: "execution-failure"; readonly diagnostics: readonly WorldControlDiagnosticDto[] };
|
|
28
|
+
|
|
29
|
+
export interface PinCandidateDto {
|
|
30
|
+
readonly constraintId: string;
|
|
31
|
+
readonly endpoint: PinEndpointRole;
|
|
32
|
+
readonly nodeId: string;
|
|
33
|
+
readonly beforeDigest: string;
|
|
34
|
+
readonly afterDigest: string;
|
|
35
|
+
readonly changed: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const compareStrings = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
|
|
39
|
+
|
|
40
|
+
const serialize = (set: ConstraintDeclarationSet, replacement: ConstraintRecord): unknown => ({
|
|
41
|
+
schemaVersion: set.schemaVersion,
|
|
42
|
+
constraints: set.records
|
|
43
|
+
.map((record) => (record.constraintId.equals(replacement.constraintId) ? replacement : record))
|
|
44
|
+
.sort((left, right) => compareStrings(left.constraintId.toString(), right.constraintId.toString()))
|
|
45
|
+
.map((record) => ({
|
|
46
|
+
constraintId: record.constraintId.toString(),
|
|
47
|
+
factType: record.factType,
|
|
48
|
+
claimant: record.claimant.toCanonicalValue(),
|
|
49
|
+
premise: record.premise.toCanonicalValue(),
|
|
50
|
+
applicableRuleIds: record.applicableRuleIds.map((ruleId) => ruleId.toString()),
|
|
51
|
+
})),
|
|
52
|
+
aliases: set.aliases
|
|
53
|
+
.map((alias) => ({ from: alias.from.toString(), to: alias.to.toString() }))
|
|
54
|
+
.sort((left, right) => compareStrings(left.from, right.from)),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const resolveNode = (snapshot: Snapshot, set: ConstraintDeclarationSet, nodeId: WorldNodeId) => {
|
|
58
|
+
const duplicate = snapshot.extractionDiagnostics.some(
|
|
59
|
+
(diagnostic) => diagnostic.code === "duplicate-node-id" && diagnostic.nodeId?.equals(nodeId),
|
|
60
|
+
);
|
|
61
|
+
if (duplicate) return { ok: false as const, code: "duplicate-endpoint", message: `duplicate endpoint: ${nodeId}` };
|
|
62
|
+
const exact = snapshot.nodes.filter((node) => node.id.equals(nodeId));
|
|
63
|
+
if (exact.length === 1) return { ok: true as const, node: exact[0] };
|
|
64
|
+
const aliases = set.aliases.filter((alias) => alias.from.equals(nodeId));
|
|
65
|
+
if (aliases.length !== 1 || set.aliases.some((alias) => alias.from.equals(aliases[0]?.to))) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false as const,
|
|
68
|
+
code: aliases.length === 0 ? "missing-endpoint" : "ambiguous-endpoint-alias",
|
|
69
|
+
message: `endpoint cannot be resolved uniquely: ${nodeId}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const targets = snapshot.nodes.filter((node) => node.id.equals(aliases[0].to));
|
|
73
|
+
return targets.length === 1
|
|
74
|
+
? { ok: true as const, node: targets[0] }
|
|
75
|
+
: {
|
|
76
|
+
ok: false as const,
|
|
77
|
+
code: targets.length === 0 ? "missing-endpoint" : "duplicate-endpoint",
|
|
78
|
+
message: `alias target cannot be resolved uniquely: ${aliases[0].to}`,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export class PinConstraintEndpointUseCase {
|
|
83
|
+
constructor(
|
|
84
|
+
private readonly buildSnapshot: BuildSnapshotContract,
|
|
85
|
+
private readonly repository: ConstraintDeclarationRepositoryPort,
|
|
86
|
+
) {}
|
|
87
|
+
|
|
88
|
+
async execute(input: PinConstraintEndpointInput): Promise<PinConstraintEndpointResult> {
|
|
89
|
+
let constraintId: WorldNodeId;
|
|
90
|
+
try {
|
|
91
|
+
constraintId = WorldNodeId.parse(input.constraintId);
|
|
92
|
+
if (constraintId.nodeType !== "constraint") throw new Error("not constraint ID");
|
|
93
|
+
} catch {
|
|
94
|
+
return { status: "domain-failure", code: "invalid-constraint-id", message: input.constraintId };
|
|
95
|
+
}
|
|
96
|
+
const [snapshot, loaded] = await Promise.all([this.buildSnapshot.execute(), this.repository.load()]);
|
|
97
|
+
if (loaded.state === "invalid") return { status: "execution-failure", diagnostics: loaded.diagnostics };
|
|
98
|
+
if (loaded.value.malformedDeclarations.length > 0 || loaded.value.diagnostics.length > 0) {
|
|
99
|
+
return {
|
|
100
|
+
status: "domain-failure",
|
|
101
|
+
code: "malformed-constraint-declaration",
|
|
102
|
+
message: "constraint declarations contain malformed or ambiguous candidates",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const records = loaded.value.records.filter((record) => record.constraintId.equals(constraintId));
|
|
106
|
+
if (records.length !== 1) {
|
|
107
|
+
return { status: "domain-failure", code: "constraint-not-found", message: input.constraintId };
|
|
108
|
+
}
|
|
109
|
+
const record = records[0];
|
|
110
|
+
const before = input.endpoint === "claimant" ? record.claimant : record.premise;
|
|
111
|
+
const resolved = resolveNode(snapshot, loaded.value, before.nodeId);
|
|
112
|
+
if (!resolved.ok) return { status: "domain-failure", code: resolved.code, message: resolved.message };
|
|
113
|
+
const after = NodePin.create({ nodeId: before.nodeId, contentDigest: resolved.node.contentDigest });
|
|
114
|
+
const replacement = ConstraintRecord.create({
|
|
115
|
+
constraintId: record.constraintId,
|
|
116
|
+
schemaVersion: record.schemaVersion,
|
|
117
|
+
factType: record.factType,
|
|
118
|
+
claimant: input.endpoint === "claimant" ? after : record.claimant,
|
|
119
|
+
premise: input.endpoint === "premise" ? after : record.premise,
|
|
120
|
+
applicableRuleIds: record.applicableRuleIds,
|
|
121
|
+
declarationArtifactId: record.declarationArtifactId,
|
|
122
|
+
declarationLocator: record.declarationLocator,
|
|
123
|
+
});
|
|
124
|
+
const candidate = Object.freeze({
|
|
125
|
+
constraintId: input.constraintId,
|
|
126
|
+
endpoint: input.endpoint,
|
|
127
|
+
nodeId: before.nodeId.toString(),
|
|
128
|
+
beforeDigest: before.contentDigest.toString(),
|
|
129
|
+
afterDigest: after.contentDigest.toString(),
|
|
130
|
+
changed: !before.contentDigest.equals(after.contentDigest),
|
|
131
|
+
});
|
|
132
|
+
if (!input.apply) return { status: "preview", candidate };
|
|
133
|
+
if (!candidate.changed) return { status: "unchanged", candidate };
|
|
134
|
+
const written = await this.repository.replaceAtomically(serialize(loaded.value, replacement));
|
|
135
|
+
return written.state === "written"
|
|
136
|
+
? { status: "applied", candidate }
|
|
137
|
+
: { status: "execution-failure", diagnostics: written.diagnostics };
|
|
138
|
+
}
|
|
139
|
+
}
|