@echopath-labs/forgerail 0.1.0-alpha.1 → 0.1.0-alpha.2
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/.codex-plugin/plugin.json +7 -3
- package/CHANGELOG.md +8 -0
- package/PLUGIN.md +2 -1
- package/PRIVACY.md +15 -0
- package/README.md +14 -2
- package/README.zh-CN.md +13 -2
- package/TERMS.md +19 -0
- package/adapters/codex.json +1 -1
- package/assets/forgerail-icon.svg +6 -0
- package/assets/forgerail-logo.svg +7 -0
- package/contracts/authority-evidence-v1.schema.json +48 -0
- package/contracts/cross-workspace-pack-composition-v1.schema.json +241 -0
- package/contracts/effective-profile-v2.schema.json +113 -0
- package/contracts/entry-mode-v1.schema.json +20 -0
- package/contracts/evidence-identity-v1.schema.json +48 -0
- package/contracts/execution-context-identity-v1.schema.json +76 -0
- package/contracts/gate-result-v1.schema.json +32 -0
- package/contracts/governance-source-v1.schema.json +30 -0
- package/contracts/host-adapter-observation-v1.schema.json +61 -0
- package/contracts/limited-reason-v1.schema.json +46 -0
- package/contracts/operation-authority-requirement-v1.schema.json +43 -0
- package/contracts/operation-grant-v1.schema.json +83 -0
- package/contracts/phase-slice-correlation-v1.schema.json +20 -0
- package/contracts/profile-explanation-v1.schema.json +91 -0
- package/contracts/provider-adapter-observation-v1.schema.json +87 -0
- package/contracts/return-receipt-v2.schema.json +55 -0
- package/contracts/review-authority-requirement-v1.schema.json +117 -0
- package/contracts/rollback-envelope-lineage-v1.schema.json +46 -0
- package/contracts/rule-claim-v1.schema.json +34 -0
- package/contracts/source-dependency-edge-v1.schema.json +37 -0
- package/contracts/task-control-revision-v1.schema.json +39 -0
- package/contracts/task-envelope-v2.schema.json +67 -0
- package/contracts/validation-result-v1.schema.json +57 -0
- package/contracts/validation-topology-v1.schema.json +55 -0
- package/contracts/workspace-identity-v1.schema.json +28 -0
- package/contracts/workspace-relationship-v1.schema.json +31 -0
- package/directory/README.md +23 -0
- package/directory/evaluations.json +78 -0
- package/directory/release-notes-alpha2.md +7 -0
- package/directory/submission-candidate.json +80 -0
- package/docs/composable-autonomy.zh-CN.md +183 -0
- package/docs/control-authority-validation-contracts.md +36 -0
- package/docs/control-authority-validation-contracts.zh-CN.md +36 -0
- package/docs/control-profile-contracts.md +35 -0
- package/docs/control-profile-contracts.zh-CN.md +35 -0
- package/docs/control-system-fixture-matrix.md +30 -0
- package/docs/control-system-fixture-matrix.zh-CN.md +30 -0
- package/docs/control-task-contracts.md +39 -0
- package/docs/control-task-contracts.zh-CN.md +39 -0
- package/docs/cross-workspace-pack-composition-contract.md +31 -0
- package/docs/cross-workspace-pack-composition-contract.zh-CN.md +31 -0
- package/docs/installation.md +9 -3
- package/docs/installation.zh-CN.md +9 -3
- package/docs/release-alpha2.md +21 -0
- package/docs/release-alpha2.zh-CN.md +21 -0
- package/package.json +9 -3
- package/packs/architecture-convergence-audit.json +30 -0
- package/scripts/build-universal-directory-candidate.mjs +72 -0
- package/scripts/disposable-consumer.mjs +1 -1
- package/scripts/fixtures/contracts/authority-evidence.revoked-without-invalidation.invalid.json +22 -0
- package/scripts/fixtures/contracts/authority-evidence.revoked.valid.json +32 -0
- package/scripts/fixtures/contracts/authority-evidence.valid.json +22 -0
- package/scripts/fixtures/contracts/capability-pack.missing-entry.invalid.json +14 -0
- package/scripts/fixtures/contracts/capability-pack.valid.json +15 -0
- package/scripts/fixtures/contracts/cross-workspace-pack-composition.authority-collapse.invalid.json +156 -0
- package/scripts/fixtures/contracts/cross-workspace-pack-composition.false-closure.invalid.json +138 -0
- package/scripts/fixtures/contracts/cross-workspace-pack-composition.valid.json +156 -0
- package/scripts/fixtures/contracts/effective-profile-v2.optional-unavailable-degraded.valid.json +36 -0
- package/scripts/fixtures/contracts/effective-profile-v2.required-unavailable-degraded.invalid.json +36 -0
- package/scripts/fixtures/contracts/effective-profile-v2.unresolved-claim-false-complete.invalid.json +34 -0
- package/scripts/fixtures/contracts/effective-profile-v2.valid.json +53 -0
- package/scripts/fixtures/contracts/effective-profile.duplicate-rule.invalid.json +23 -0
- package/scripts/fixtures/contracts/entry-mode.missing-evidence.invalid.json +6 -0
- package/scripts/fixtures/contracts/entry-mode.valid.json +6 -0
- package/scripts/fixtures/contracts/evidence-identity.unavailable-without-reason.invalid.json +21 -0
- package/scripts/fixtures/contracts/evidence-identity.valid.json +21 -0
- package/scripts/fixtures/contracts/execution-context-identity.unavailable-without-reason.invalid.json +31 -0
- package/scripts/fixtures/contracts/execution-context-identity.valid.json +31 -0
- package/scripts/fixtures/contracts/gate-result.satisfied-without-evidence.invalid.json +14 -0
- package/scripts/fixtures/contracts/gate-result.valid.json +14 -0
- package/scripts/fixtures/contracts/governance-source.unavailable-without-reason.invalid.json +16 -0
- package/scripts/fixtures/contracts/governance-source.valid.json +16 -0
- package/scripts/fixtures/contracts/host-adapter-observation.claims-authorization.invalid.json +23 -0
- package/scripts/fixtures/contracts/host-adapter-observation.valid.json +39 -0
- package/scripts/fixtures/contracts/host-adapter.codex.valid.json +1 -1
- package/scripts/fixtures/contracts/host-adapter.false-supported.invalid.json +14 -0
- package/scripts/fixtures/contracts/host-binding-receipt.valid.json +1 -1
- package/scripts/fixtures/contracts/launch-contract.execution-owner.invalid.json +19 -0
- package/scripts/fixtures/contracts/limited-reason.unbounded.invalid.json +11 -0
- package/scripts/fixtures/contracts/limited-reason.valid.json +11 -0
- package/scripts/fixtures/contracts/operation-authority-requirement.no-issuer.invalid.json +22 -0
- package/scripts/fixtures/contracts/operation-authority-requirement.valid.json +22 -0
- package/scripts/fixtures/contracts/operation-grant.missing-issuer-evidence.invalid.json +34 -0
- package/scripts/fixtures/contracts/operation-grant.valid.json +34 -0
- package/scripts/fixtures/contracts/phase-slice-correlation.aggregate-closure.invalid.json +12 -0
- package/scripts/fixtures/contracts/phase-slice-correlation.valid.json +12 -0
- package/scripts/fixtures/contracts/profile-change-candidate.unconfirmed.invalid.json +13 -0
- package/scripts/fixtures/contracts/profile-explanation.false-complete.invalid.json +36 -0
- package/scripts/fixtures/contracts/profile-explanation.valid.json +29 -0
- package/scripts/fixtures/contracts/provider-adapter-observation.unauthenticated-supported.invalid.json +43 -0
- package/scripts/fixtures/contracts/provider-adapter-observation.valid.json +62 -0
- package/scripts/fixtures/contracts/return-receipt-v2.changed-subject.valid.json +36 -0
- package/scripts/fixtures/contracts/return-receipt-v2.unverified-closeout.invalid.json +36 -0
- package/scripts/fixtures/contracts/return-receipt-v2.valid.json +36 -0
- package/scripts/fixtures/contracts/review-authority-requirement.transitive.invalid.json +33 -0
- package/scripts/fixtures/contracts/review-authority-requirement.valid.json +21 -0
- package/scripts/fixtures/contracts/rollback-envelope-lineage.reused-grant.invalid.json +24 -0
- package/scripts/fixtures/contracts/rollback-envelope-lineage.valid.json +24 -0
- package/scripts/fixtures/contracts/rule-claim.inferred-enforceable.invalid.json +16 -0
- package/scripts/fixtures/contracts/rule-claim.valid.json +16 -0
- package/scripts/fixtures/contracts/source-dependency-edge.unavailable-without-reason.invalid.json +18 -0
- package/scripts/fixtures/contracts/source-dependency-edge.valid.json +18 -0
- package/scripts/fixtures/contracts/task-control-revision.closed-without-receipt.invalid.json +37 -0
- package/scripts/fixtures/contracts/task-control-revision.valid.json +37 -0
- package/scripts/fixtures/contracts/task-envelope-v2.missing-workspace-identity.invalid.json +39 -0
- package/scripts/fixtures/contracts/task-envelope-v2.valid.json +54 -0
- package/scripts/fixtures/contracts/task-envelope-v2.workspace-mismatch.invalid.json +40 -0
- package/scripts/fixtures/contracts/validation-result.passed-without-evidence.invalid.json +29 -0
- package/scripts/fixtures/contracts/validation-result.valid.json +19 -0
- package/scripts/fixtures/contracts/validation-topology.dangling-requirement.invalid.json +32 -0
- package/scripts/fixtures/contracts/validation-topology.protected-environment.valid.json +32 -0
- package/scripts/fixtures/contracts/validation-topology.valid.json +32 -0
- package/scripts/fixtures/contracts/workspace-identity.missing-root.invalid.json +6 -0
- package/scripts/fixtures/contracts/workspace-identity.valid.json +14 -0
- package/scripts/fixtures/contracts/workspace-relationship.inferred-authority.invalid.json +13 -0
- package/scripts/fixtures/contracts/workspace-relationship.valid.json +13 -0
- package/scripts/fixtures/control-system/architecture-convergence-cases.json +25 -0
- package/scripts/fixtures/control-system/task-2.9-invalid-boundary-matrix.json +203 -0
- package/scripts/fixtures/control-system/task-4-diagnosis-health-separation.json +19 -0
- package/scripts/fixtures/workspaces/no-node-project/AGENTS.md +3 -0
- package/scripts/fixtures/workspaces/no-node-project/README.md +3 -0
- package/scripts/fixtures/workspaces/no-node-project/docs/adr/0001-durable-records.md +3 -0
- package/scripts/forgerail.mjs +467 -14
- package/scripts/lib/contracts.mjs +1132 -2
- package/scripts/shadow-comparison.mjs +1 -1
- package/scripts/validate-release.mjs +15 -15
- package/scripts/validate-universal-directory.mjs +151 -0
- package/skills/architecture-convergence-audit/SKILL.md +31 -0
- package/skills/architecture-convergence-audit/agents/openai.yaml +7 -0
- package/skills/architecture-convergence-audit/assets/github-issue-template.md +26 -0
- package/skills/architecture-convergence-audit/references/audit-contract.md +62 -0
- package/skills/workspace-health-review/references/health-review.md +15 -0
package/scripts/forgerail.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { loadHostAdapters, planAdoption } from "./lib/adoption.mjs";
|
|
8
8
|
import { buildBundle } from "./lib/bundle.mjs";
|
|
9
9
|
import { createLaunchContract, resolveProfile, verifyReceipt } from "./lib/composition.mjs";
|
|
10
|
-
import { contractTypes, readJson, validateContract } from "./lib/contracts.mjs";
|
|
10
|
+
import { contractSchemaNames, contractTypes, readJson, validateContract } from "./lib/contracts.mjs";
|
|
11
11
|
import { diagnoseWorkspace } from "./lib/diagnosis.mjs";
|
|
12
12
|
|
|
13
13
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -21,6 +21,199 @@ function args(name) {
|
|
|
21
21
|
return values;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function collectSchemaRefs(value, refs = []) {
|
|
25
|
+
if (Array.isArray(value)) value.forEach((item) => collectSchemaRefs(item, refs));
|
|
26
|
+
else if (value && typeof value === "object") {
|
|
27
|
+
if (typeof value.$ref === "string") refs.push(value.$ref);
|
|
28
|
+
Object.values(value).forEach((item) => collectSchemaRefs(item, refs));
|
|
29
|
+
}
|
|
30
|
+
return refs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sameMembers(actual, expected) {
|
|
34
|
+
return Array.isArray(actual) && actual.length === expected.length && expected.every((item) => actual.includes(item));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function validateEffectiveProfileV2Schema(schema, errors) {
|
|
38
|
+
const assertions = [];
|
|
39
|
+
const rules = Array.isArray(schema?.allOf) ? schema.allOf : [];
|
|
40
|
+
const nonAvailable = ["unavailable", "ambiguous", "unverified"];
|
|
41
|
+
const completeRule = rules.find((rule) => rule.if?.properties?.completeness?.const === "complete");
|
|
42
|
+
const completeClaims = completeRule?.then?.properties?.ruleClaims?.not?.contains;
|
|
43
|
+
if (completeClaims?.properties?.enforcement?.const === "unresolved" && completeClaims.required?.includes("enforcement")) assertions.push("complete_excludes_unresolved_claim");
|
|
44
|
+
else errors.push("effective-profile-v2 schema must natively exclude unresolved claims from complete Profiles");
|
|
45
|
+
const completeDependencies = completeRule?.then?.properties?.dependencyEdges?.not?.contains;
|
|
46
|
+
if (sameMembers(completeDependencies?.properties?.observationStatus?.enum, nonAvailable) && completeDependencies.required?.includes("observationStatus")) assertions.push("complete_excludes_non_available_dependency");
|
|
47
|
+
else errors.push("effective-profile-v2 schema must natively exclude non-available dependencies from complete Profiles");
|
|
48
|
+
|
|
49
|
+
const unresolvedClaimRule = rules.find((rule) => rule.if?.properties?.ruleClaims?.contains?.properties?.enforcement?.const === "unresolved");
|
|
50
|
+
if (unresolvedClaimRule?.if?.properties?.ruleClaims?.contains?.required?.includes("enforcement") && unresolvedClaimRule?.then?.properties?.completeness?.const === "unresolved") assertions.push("unresolved_claim_forces_unresolved_profile");
|
|
51
|
+
else errors.push("effective-profile-v2 schema must natively require unresolved completeness for unresolved claims");
|
|
52
|
+
|
|
53
|
+
const requiredDependencyRule = rules.find((rule) => {
|
|
54
|
+
const clauses = rule.if?.properties?.dependencyEdges?.contains?.allOf;
|
|
55
|
+
return Array.isArray(clauses)
|
|
56
|
+
&& clauses.some((clause) => clause.properties?.requiredness?.const === "required" && clause.required?.includes("requiredness"))
|
|
57
|
+
&& clauses.some((clause) => sameMembers(clause.properties?.observationStatus?.enum, nonAvailable) && clause.required?.includes("observationStatus"));
|
|
58
|
+
});
|
|
59
|
+
if (requiredDependencyRule?.then?.properties?.completeness?.const === "unresolved") {
|
|
60
|
+
assertions.push("required_non_available_dependency_forces_unresolved_profile");
|
|
61
|
+
assertions.push("optional_non_available_dependency_may_remain_degraded");
|
|
62
|
+
} else errors.push("effective-profile-v2 schema must natively require unresolved completeness only for required non-available dependencies");
|
|
63
|
+
|
|
64
|
+
const conflictRule = rules.find((rule) => rule.if?.properties?.conflicts?.minItems === 1);
|
|
65
|
+
if (conflictRule?.then?.properties?.completeness?.const === "unresolved") assertions.push("conflict_forces_unresolved_profile");
|
|
66
|
+
else errors.push("effective-profile-v2 schema must natively require unresolved completeness for non-empty conflicts");
|
|
67
|
+
return assertions;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateTaskControlSchemas(schemas, errors) {
|
|
71
|
+
const assertions = [];
|
|
72
|
+
const byType = new Map(schemas.map(({ type, schema }) => [type, schema]));
|
|
73
|
+
const envelope = byType.get("task-envelope-v2");
|
|
74
|
+
if (envelope?.properties?.entryMode?.$ref === "https://forgerail.dev/schemas/entry-mode-v1.json"
|
|
75
|
+
&& envelope?.properties?.phaseSliceCorrelation?.$ref === "https://forgerail.dev/schemas/phase-slice-correlation-v1.json"
|
|
76
|
+
&& envelope?.properties?.returnContract?.const === "return-receipt-v2") assertions.push("envelope_binds_entry_slice_and_receipt_version");
|
|
77
|
+
else errors.push("task-envelope-v2 schema must bind versioned entry mode, phase/slice correlation, and Return Receipt v2");
|
|
78
|
+
|
|
79
|
+
const grant = byType.get("operation-grant");
|
|
80
|
+
const grantRequired = grant?.required ?? [];
|
|
81
|
+
const currentRule = grant?.allOf?.find((rule) => rule.if?.properties?.status?.const === "current");
|
|
82
|
+
if (["grantDigest", "authorityRequirementId", "workspaceIdentityId", "envelopeRevisionId", "executorId", "operation", "target", "subjectId", "scope", "issuerEvidenceId", "expiresAt"].every((key) => grantRequired.includes(key))
|
|
83
|
+
&& currentRule?.then?.properties?.invalidatedAt?.const === null) assertions.push("grant_binds_exact_authority_and_current_state");
|
|
84
|
+
else errors.push("operation-grant-v1 schema must bind exact authority fields and exclude invalidation from current grants");
|
|
85
|
+
|
|
86
|
+
const controlRevision = byType.get("task-control-revision");
|
|
87
|
+
const closedRule = controlRevision?.allOf?.find((rule) => rule.if?.properties?.state?.const === "closed");
|
|
88
|
+
if (controlRevision?.properties?.entryMode?.$ref === "https://forgerail.dev/schemas/entry-mode-v1.json"
|
|
89
|
+
&& controlRevision?.properties?.phaseSliceCorrelation?.$ref === "https://forgerail.dev/schemas/phase-slice-correlation-v1.json"
|
|
90
|
+
&& closedRule?.then?.properties?.returnReceiptId?.type === "string"
|
|
91
|
+
&& closedRule?.then?.properties?.deviationEvidenceIds?.maxItems === 0) assertions.push("closed_revision_requires_receipt_without_deviation");
|
|
92
|
+
else errors.push("task-control-revision-v1 schema must bind entry/slice identity and require a deviation-free receipt for closure");
|
|
93
|
+
|
|
94
|
+
const receipt = byType.get("return-receipt-v2");
|
|
95
|
+
const completeRule = receipt?.allOf?.find((rule) => rule.if?.properties?.closeout?.const === "complete");
|
|
96
|
+
if (receipt?.properties?.phaseSliceCorrelation?.$ref === "https://forgerail.dev/schemas/phase-slice-correlation-v1.json"
|
|
97
|
+
&& completeRule?.then?.properties?.claimStatus?.const === "verified"
|
|
98
|
+
&& completeRule?.then?.properties?.deviations?.maxItems === 0) assertions.push("complete_receipt_requires_verification_without_deviation");
|
|
99
|
+
else errors.push("return-receipt-v2 schema must require verified, deviation-free exact-slice closure");
|
|
100
|
+
|
|
101
|
+
const rollback = byType.get("rollback-envelope-lineage");
|
|
102
|
+
const verifiedRule = rollback?.allOf?.find((rule) => rule.if?.properties?.state?.const === "verified");
|
|
103
|
+
if (rollback?.properties?.forwardEnvelope?.$ref === "#/$defs/envelopeReference"
|
|
104
|
+
&& rollback?.properties?.rollbackEnvelope?.$ref === "#/$defs/envelopeReference"
|
|
105
|
+
&& verifiedRule?.then?.properties?.validationGateResultIds?.minItems === 1
|
|
106
|
+
&& verifiedRule?.then?.properties?.rollbackReceiptId?.type === "string") assertions.push("rollback_lineage_requires_new_control_evidence");
|
|
107
|
+
else errors.push("rollback-envelope-lineage-v1 schema must separate Envelope references and require rollback verification evidence");
|
|
108
|
+
return assertions;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function validateTask27Schemas(schemas, errors) {
|
|
112
|
+
const assertions = [];
|
|
113
|
+
const byType = new Map(schemas.map(({ type, schema }) => [type, schema]));
|
|
114
|
+
|
|
115
|
+
const limitedReason = byType.get("limited-reason");
|
|
116
|
+
if ((limitedReason?.properties?.code?.enum?.length ?? 0) >= 10
|
|
117
|
+
&& limitedReason?.properties?.sanitized?.const === true
|
|
118
|
+
&& limitedReason?.properties?.summary?.maxLength === 500) assertions.push("limited_reason_is_bounded_and_sanitized");
|
|
119
|
+
else errors.push("limited-reason-v1 schema must bound reason codes, summary exposure, and sanitization");
|
|
120
|
+
|
|
121
|
+
const reviewRequirement = byType.get("review-authority-requirement");
|
|
122
|
+
const noSubstitutionRule = reviewRequirement?.allOf?.find((rule) => rule.if?.properties?.substitutionPolicy?.properties?.mode?.const === "none");
|
|
123
|
+
if (reviewRequirement?.properties?.quorum?.properties?.minimumCount?.minimum === 1
|
|
124
|
+
&& reviewRequirement?.properties?.ownerCoverage?.properties?.surfaceIds
|
|
125
|
+
&& noSubstitutionRule?.then?.properties?.substitutionPolicy?.properties?.equivalenceEdges?.maxItems === 0) assertions.push("review_authority_binds_quorum_owner_and_non_substitution");
|
|
126
|
+
else errors.push("review-authority-requirement-v1 schema must bind quorum, owner coverage, and default non-substitution");
|
|
127
|
+
|
|
128
|
+
const authorityEvidence = byType.get("authority-evidence");
|
|
129
|
+
const currentEvidenceRule = authorityEvidence?.allOf?.find((rule) => rule.if?.properties?.lifecycle?.const === "current");
|
|
130
|
+
const lifecycle = authorityEvidence?.properties?.lifecycle?.enum ?? [];
|
|
131
|
+
if (["current", "stale", "revoked", "dismissed", "expired", "superseded"].every((state) => lifecycle.includes(state))
|
|
132
|
+
&& currentEvidenceRule?.then?.properties?.limitedReason?.const === null) assertions.push("authority_evidence_lifecycle_is_explicit_and_current_only");
|
|
133
|
+
else errors.push("authority-evidence-v1 schema must expose the complete lifecycle and exclude invalidation from current evidence");
|
|
134
|
+
|
|
135
|
+
const topology = byType.get("validation-topology");
|
|
136
|
+
if (topology?.properties?.changedSurfaces?.items?.properties?.validationRequirementIds
|
|
137
|
+
&& topology?.properties?.validationRequirements?.items?.properties?.prerequisiteDependencyEdgeIds
|
|
138
|
+
&& topology?.properties?.validationRequirements?.items?.properties?.acceptedTrustClasses
|
|
139
|
+
&& topology?.properties?.validationRequirements?.items?.properties?.expectedResults) assertions.push("validation_topology_links_surface_dependency_trust_and_expected_result");
|
|
140
|
+
else errors.push("validation-topology-v1 schema must link changed surfaces to prerequisites, trust, and expected results");
|
|
141
|
+
|
|
142
|
+
const validationResult = byType.get("validation-result");
|
|
143
|
+
const resultStates = validationResult?.properties?.status?.enum ?? [];
|
|
144
|
+
const passedRule = validationResult?.allOf?.find((rule) => rule.if?.properties?.status?.const === "passed");
|
|
145
|
+
if (["passed", "failed", "blocked", "unavailable", "not_applicable", "not_selected", "waived"].every((state) => resultStates.includes(state))
|
|
146
|
+
&& passedRule?.then?.properties?.evidenceIdentityIds?.minItems === 1
|
|
147
|
+
&& passedRule?.then?.properties?.limitedReason?.const === null) assertions.push("validation_result_taxonomy_preserves_evidence_and_truthful_pass");
|
|
148
|
+
else errors.push("validation-result-v1 schema must expose the complete taxonomy and require evidence for passed results");
|
|
149
|
+
|
|
150
|
+
const executionContext = byType.get("execution-context-identity");
|
|
151
|
+
const dependency = executionContext?.properties?.dependencies?.items;
|
|
152
|
+
if (executionContext?.properties?.entrypoint
|
|
153
|
+
&& executionContext?.properties?.invocationRoot
|
|
154
|
+
&& executionContext?.properties?.runner?.properties?.trustClass
|
|
155
|
+
&& dependency?.properties?.boundary
|
|
156
|
+
&& dependency?.properties?.limitedReason) assertions.push("execution_context_binds_entrypoint_runner_and_external_dependencies");
|
|
157
|
+
else errors.push("execution-context-identity-v1 schema must bind entrypoint, runner trust, and external dependencies");
|
|
158
|
+
|
|
159
|
+
const hostObservation = byType.get("host-adapter-observation");
|
|
160
|
+
if (hostObservation?.properties?.authorizationClaim?.const === false
|
|
161
|
+
&& hostObservation?.properties?.capabilities?.items?.$ref === "#/$defs/capabilityObservation"
|
|
162
|
+
&& hostObservation?.$defs?.capabilityObservation?.properties?.state?.enum?.includes("profile-only")) assertions.push("host_observation_separates_capability_from_authority");
|
|
163
|
+
else errors.push("host-adapter-observation-v1 schema must separate capability observation from authorization");
|
|
164
|
+
|
|
165
|
+
const providerObservation = byType.get("provider-adapter-observation");
|
|
166
|
+
const identityStates = providerObservation?.properties?.identity?.properties?.state?.enum ?? [];
|
|
167
|
+
if (providerObservation?.properties?.authorizationClaim?.const === false
|
|
168
|
+
&& ["authenticated", "unauthenticated", "unverified", "unavailable", "not-required"].every((state) => identityStates.includes(state))
|
|
169
|
+
&& providerObservation?.properties?.capabilities?.items?.properties?.requiresIdentity) assertions.push("provider_observation_separates_identity_capability_and_authority");
|
|
170
|
+
else errors.push("provider-adapter-observation-v1 schema must separate identity, capability, and authorization");
|
|
171
|
+
return assertions;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function validateTask28Schemas(schemas, errors) {
|
|
175
|
+
const assertions = [];
|
|
176
|
+
const composition = schemas.find(({ type }) => type === "cross-workspace-pack-composition")?.schema;
|
|
177
|
+
const compatibility = composition?.$defs?.coreCompatibility;
|
|
178
|
+
const node = composition?.$defs?.node;
|
|
179
|
+
if (compatibility?.properties?.workspaceIdentityVersions?.minItems === 1
|
|
180
|
+
&& compatibility?.properties?.taskEnvelopeVersions?.minItems === 1
|
|
181
|
+
&& compatibility?.properties?.returnReceiptVersions?.minItems === 1
|
|
182
|
+
&& compatibility?.properties?.phaseSliceCorrelationVersions?.minItems === 1
|
|
183
|
+
&& node?.properties?.workspaceIdentitySchemaVersion
|
|
184
|
+
&& node?.properties?.coreEnvelope?.$ref === "#/$defs/coreEnvelopeReference") assertions.push("pack_composition_declares_version_compatible_core_contracts");
|
|
185
|
+
else errors.push("cross-workspace-pack-composition-v1 must declare version compatibility for every referenced Core contract");
|
|
186
|
+
|
|
187
|
+
const invariants = composition?.$defs?.kernelInvariants?.properties ?? {};
|
|
188
|
+
const dependency = composition?.$defs?.dependencyEdge?.properties ?? {};
|
|
189
|
+
if (["stateModelRedefined", "operationGrantMinting", "authoritySubstitution", "waiverBroadening", "freshnessBroadening", "receiptVerificationBypass", "transportAsAcceptance"].every((key) => invariants[key]?.const === false)
|
|
190
|
+
&& dependency.authorityTransfer?.const === false
|
|
191
|
+
&& dependency.operationGrantTransfer?.const === false
|
|
192
|
+
&& dependency.receiptSubstitution?.const === false) assertions.push("pack_composition_cannot_override_kernel_or_transfer_authority");
|
|
193
|
+
else errors.push("cross-workspace-pack-composition-v1 must prohibit Kernel overrides and cross-node authority transfer");
|
|
194
|
+
|
|
195
|
+
const receipt = composition?.$defs?.coreReceiptReference?.anyOf?.find((item) => item.type === "object");
|
|
196
|
+
const acceptedReceiptRule = receipt?.allOf?.find((rule) => rule.if?.properties?.coordinatorAcceptance?.const === "accepted");
|
|
197
|
+
if (receipt?.properties?.verificationStatus
|
|
198
|
+
&& receipt?.properties?.transportStatus
|
|
199
|
+
&& receipt?.properties?.coordinatorAcceptance
|
|
200
|
+
&& acceptedReceiptRule?.then?.properties?.verificationStatus?.const === "verified"
|
|
201
|
+
&& acceptedReceiptRule?.then?.properties?.coordinatorAcceptanceEvidenceIdentityIds?.minItems === 1) assertions.push("core_verification_transport_and_acceptance_remain_independent");
|
|
202
|
+
else errors.push("cross-workspace-pack-composition-v1 must separate Core verification, transport, and coordinator acceptance");
|
|
203
|
+
|
|
204
|
+
const phase = composition?.$defs?.phaseAggregation;
|
|
205
|
+
const closedPhaseRule = phase?.allOf?.find((rule) => rule.if?.properties?.state?.const === "closed");
|
|
206
|
+
const bundle = composition?.$defs?.workspaceReceiptBundle;
|
|
207
|
+
const acceptedBundleRule = bundle?.allOf?.find((rule) => rule.if?.properties?.state?.const === "accepted");
|
|
208
|
+
if (closedPhaseRule?.then?.properties?.aggregateClosureClaim?.const === true
|
|
209
|
+
&& ["pendingNodeIds", "failedNodeIds", "blockedNodeIds", "staleNodeIds"].every((key) => closedPhaseRule?.then?.properties?.[key]?.maxItems === 0)
|
|
210
|
+
&& bundle?.properties?.immutable?.const === true
|
|
211
|
+
&& ["pendingNodeIds", "failedNodeIds", "blockedNodeIds", "staleNodeIds"].every((key) => acceptedBundleRule?.then?.properties?.[key]?.maxItems === 0)
|
|
212
|
+
&& acceptedBundleRule?.then?.properties?.deviationEvidenceIdentityIds?.maxItems === 0) assertions.push("phase_and_workspace_bundle_close_only_without_unresolved_nodes");
|
|
213
|
+
else errors.push("cross-workspace-pack-composition-v1 must make phase and bundle closure exact and immutable");
|
|
214
|
+
return assertions;
|
|
215
|
+
}
|
|
216
|
+
|
|
24
217
|
function workspaceSnapshot(path) {
|
|
25
218
|
return readdirSync(path, { recursive: true })
|
|
26
219
|
.sort()
|
|
@@ -36,9 +229,9 @@ function validatePlugin() {
|
|
|
36
229
|
const manifestPath = resolve(root, ".codex-plugin/plugin.json");
|
|
37
230
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
38
231
|
if (manifest.name !== "forgerail") errors.push("Plugin name must be forgerail");
|
|
39
|
-
if (manifest.version !== "0.1.0-alpha.
|
|
232
|
+
if (manifest.version !== "0.1.0-alpha.2") errors.push("Plugin version must be 0.1.0-alpha.2");
|
|
40
233
|
if (manifest.license !== "Apache-2.0") errors.push("Plugin license must be Apache-2.0");
|
|
41
|
-
const expectedSkills = ["forgerail", "forgerail-workspace-diagnosis", "workspace-health-review"];
|
|
234
|
+
const expectedSkills = ["architecture-convergence-audit", "forgerail", "forgerail-workspace-diagnosis", "workspace-health-review"];
|
|
42
235
|
const actualSkills = readdirSync(resolve(root, "skills"), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
43
236
|
if (JSON.stringify(actualSkills) !== JSON.stringify(expectedSkills)) errors.push(`Expected Skills ${expectedSkills.join(", ")}; received ${actualSkills.join(", ")}`);
|
|
44
237
|
for (const skill of expectedSkills) {
|
|
@@ -51,15 +244,40 @@ function validatePlugin() {
|
|
|
51
244
|
if (!existsSync(resolve(skillRoot, match[1]))) errors.push(`${skill} reference is missing: ${match[1]}`);
|
|
52
245
|
}
|
|
53
246
|
}
|
|
247
|
+
const schemas = [];
|
|
248
|
+
const schemaRefs = [];
|
|
54
249
|
for (const type of contractTypes) {
|
|
55
|
-
const name =
|
|
56
|
-
|
|
250
|
+
const name = contractSchemaNames[type];
|
|
251
|
+
const path = resolve(root, "contracts", `${name}.schema.json`);
|
|
252
|
+
const schema = JSON.parse(readFileSync(path, "utf8"));
|
|
253
|
+
schemas.push({ type, name, schema });
|
|
254
|
+
if (schema.$schema !== "https://json-schema.org/draft/2020-12/schema") errors.push(`${name}.schema.json must use JSON Schema draft 2020-12`);
|
|
255
|
+
if (typeof schema.$id !== "string" || !/-v[0-9]+\.json$/.test(schema.$id)) errors.push(`${name}.schema.json must declare a versioned $id`);
|
|
256
|
+
if (!Object.hasOwn(schema.properties ?? {}, "schemaVersion")) errors.push(`${name}.schema.json must declare schemaVersion`);
|
|
257
|
+
for (const ref of collectSchemaRefs(schema)) {
|
|
258
|
+
schemaRefs.push({ name, ref });
|
|
259
|
+
if (ref.startsWith("#") || /^[a-z]+:/i.test(ref)) continue;
|
|
260
|
+
if (!existsSync(resolve(root, "contracts", ref))) errors.push(`${name}.schema.json has unresolved $ref: ${ref}`);
|
|
261
|
+
}
|
|
57
262
|
}
|
|
263
|
+
const schemaIds = schemas.map(({ schema }) => schema.$id).filter(Boolean);
|
|
264
|
+
if (new Set(schemaIds).size !== schemaIds.length) errors.push("contract schemas must use unique $id values");
|
|
265
|
+
const schemaIdSet = new Set(schemaIds);
|
|
266
|
+
for (const { name, ref } of schemaRefs) if (ref.startsWith("https://forgerail.dev/schemas/") && !schemaIdSet.has(ref)) errors.push(`${name}.schema.json has unresolved schema $id reference: ${ref}`);
|
|
267
|
+
const effectiveProfileV2Schema = schemas.find(({ type }) => type === "effective-profile-v2")?.schema;
|
|
268
|
+
const schemaNativeAssertions = [
|
|
269
|
+
...validateEffectiveProfileV2Schema(effectiveProfileV2Schema, errors),
|
|
270
|
+
...validateTaskControlSchemas(schemas, errors),
|
|
271
|
+
...validateTask27Schemas(schemas, errors),
|
|
272
|
+
...validateTask28Schemas(schemas, errors),
|
|
273
|
+
];
|
|
58
274
|
const adapterRegistry = loadHostAdapters(root);
|
|
59
275
|
if (!adapterRegistry.valid) errors.push(...adapterRegistry.errors);
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
276
|
+
const packPaths = readdirSync(resolve(root, "packs")).filter((name) => name.endsWith(".json")).sort();
|
|
277
|
+
for (const packName of packPaths) {
|
|
278
|
+
const packResult = validateContract("pack", readJson(resolve(root, "packs", packName)));
|
|
279
|
+
if (!packResult.valid) errors.push(...packResult.errors.map((error) => `${packName}: ${error}`));
|
|
280
|
+
}
|
|
63
281
|
const coverage = readJson(resolve(root, "docs/agw-coverage-baseline.json"));
|
|
64
282
|
const allowedDispositions = new Set(coverage.allowedDispositions);
|
|
65
283
|
if (new Set(coverage.items.map((item) => item.id)).size !== coverage.items.length) errors.push("AGW coverage contains duplicate ids");
|
|
@@ -70,32 +288,265 @@ function validatePlugin() {
|
|
|
70
288
|
}
|
|
71
289
|
if (coverage.migrationReady && coverage.items.some((item) => item.status !== "mapped")) errors.push("AGW migration cannot be ready with unresolved coverage");
|
|
72
290
|
const contextBytes = Object.fromEntries(actualSkills.map((skill) => [skill, readFileSync(resolve(root, "skills", skill, "SKILL.md")).length]));
|
|
73
|
-
|
|
291
|
+
if (contextBytes["architecture-convergence-audit"] > 5000) errors.push("architecture convergence default Skill context exceeds 5000 bytes");
|
|
292
|
+
return { valid: errors.length === 0, errors, skills: actualSkills, packs: packPaths, contracts: contractTypes, schemaNativeAssertions, contextBytes };
|
|
74
293
|
}
|
|
75
294
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
295
|
+
const contractFixtureCases = [
|
|
296
|
+
["workspace-identity", "contracts/workspace-identity.valid.json", true],
|
|
297
|
+
["workspace-identity", "contracts/workspace-identity.missing-root.invalid.json", false],
|
|
298
|
+
["workspace-relationship", "contracts/workspace-relationship.valid.json", true],
|
|
299
|
+
["workspace-relationship", "contracts/workspace-relationship.inferred-authority.invalid.json", false],
|
|
300
|
+
["governance-source", "contracts/governance-source.valid.json", true],
|
|
301
|
+
["governance-source", "contracts/governance-source.unavailable-without-reason.invalid.json", false],
|
|
302
|
+
["source-dependency-edge", "contracts/source-dependency-edge.valid.json", true],
|
|
303
|
+
["source-dependency-edge", "contracts/source-dependency-edge.unavailable-without-reason.invalid.json", false],
|
|
304
|
+
["rule-claim", "contracts/rule-claim.valid.json", true],
|
|
305
|
+
["rule-claim", "contracts/rule-claim.inferred-enforceable.invalid.json", false],
|
|
306
|
+
["effective-profile-v2", "contracts/effective-profile-v2.valid.json", true],
|
|
307
|
+
["effective-profile-v2", "contracts/effective-profile-v2.unresolved-claim-false-complete.invalid.json", false],
|
|
308
|
+
["effective-profile-v2", "contracts/effective-profile-v2.required-unavailable-degraded.invalid.json", false],
|
|
309
|
+
["effective-profile-v2", "contracts/effective-profile-v2.optional-unavailable-degraded.valid.json", true],
|
|
310
|
+
["profile-explanation", "contracts/profile-explanation.valid.json", true],
|
|
311
|
+
["profile-explanation", "contracts/profile-explanation.false-complete.invalid.json", false],
|
|
312
|
+
["entry-mode", "contracts/entry-mode.valid.json", true],
|
|
313
|
+
["entry-mode", "contracts/entry-mode.missing-evidence.invalid.json", false],
|
|
314
|
+
["phase-slice-correlation", "contracts/phase-slice-correlation.valid.json", true],
|
|
315
|
+
["phase-slice-correlation", "contracts/phase-slice-correlation.aggregate-closure.invalid.json", false],
|
|
316
|
+
["operation-authority-requirement", "contracts/operation-authority-requirement.valid.json", true],
|
|
317
|
+
["operation-authority-requirement", "contracts/operation-authority-requirement.no-issuer.invalid.json", false],
|
|
318
|
+
["evidence-identity", "contracts/evidence-identity.valid.json", true],
|
|
319
|
+
["evidence-identity", "contracts/evidence-identity.unavailable-without-reason.invalid.json", false],
|
|
320
|
+
["task-envelope-v2", "contracts/task-envelope-v2.valid.json", true],
|
|
321
|
+
["task-envelope-v2", "contracts/task-envelope-v2.workspace-mismatch.invalid.json", false],
|
|
322
|
+
["task-envelope-v2", "contracts/task-envelope-v2.missing-workspace-identity.invalid.json", false],
|
|
323
|
+
["operation-grant", "contracts/operation-grant.valid.json", true],
|
|
324
|
+
["operation-grant", "contracts/operation-grant.missing-issuer-evidence.invalid.json", false],
|
|
325
|
+
["gate-result", "contracts/gate-result.valid.json", true],
|
|
326
|
+
["gate-result", "contracts/gate-result.satisfied-without-evidence.invalid.json", false],
|
|
327
|
+
["task-control-revision", "contracts/task-control-revision.valid.json", true],
|
|
328
|
+
["task-control-revision", "contracts/task-control-revision.closed-without-receipt.invalid.json", false],
|
|
329
|
+
["return-receipt-v2", "contracts/return-receipt-v2.valid.json", true],
|
|
330
|
+
["return-receipt-v2", "contracts/return-receipt-v2.unverified-closeout.invalid.json", false],
|
|
331
|
+
["rollback-envelope-lineage", "contracts/rollback-envelope-lineage.valid.json", true],
|
|
332
|
+
["rollback-envelope-lineage", "contracts/rollback-envelope-lineage.reused-grant.invalid.json", false],
|
|
333
|
+
["limited-reason", "contracts/limited-reason.valid.json", true],
|
|
334
|
+
["limited-reason", "contracts/limited-reason.unbounded.invalid.json", false],
|
|
335
|
+
["review-authority-requirement", "contracts/review-authority-requirement.valid.json", true],
|
|
336
|
+
["review-authority-requirement", "contracts/review-authority-requirement.transitive.invalid.json", false],
|
|
337
|
+
["authority-evidence", "contracts/authority-evidence.valid.json", true],
|
|
338
|
+
["authority-evidence", "contracts/authority-evidence.revoked.valid.json", true],
|
|
339
|
+
["authority-evidence", "contracts/authority-evidence.revoked-without-invalidation.invalid.json", false],
|
|
340
|
+
["validation-topology", "contracts/validation-topology.valid.json", true],
|
|
341
|
+
["validation-topology", "contracts/validation-topology.protected-environment.valid.json", true],
|
|
342
|
+
["validation-topology", "contracts/validation-topology.dangling-requirement.invalid.json", false],
|
|
343
|
+
["validation-result", "contracts/validation-result.valid.json", true],
|
|
344
|
+
["validation-result", "contracts/validation-result.passed-without-evidence.invalid.json", false],
|
|
345
|
+
["execution-context-identity", "contracts/execution-context-identity.valid.json", true],
|
|
346
|
+
["execution-context-identity", "contracts/execution-context-identity.unavailable-without-reason.invalid.json", false],
|
|
347
|
+
["host-adapter-observation", "contracts/host-adapter-observation.valid.json", true],
|
|
348
|
+
["host-adapter-observation", "contracts/host-adapter-observation.claims-authorization.invalid.json", false],
|
|
349
|
+
["provider-adapter-observation", "contracts/provider-adapter-observation.valid.json", true],
|
|
350
|
+
["provider-adapter-observation", "contracts/provider-adapter-observation.unauthenticated-supported.invalid.json", false],
|
|
351
|
+
["cross-workspace-pack-composition", "contracts/cross-workspace-pack-composition.valid.json", true],
|
|
352
|
+
["cross-workspace-pack-composition", "contracts/cross-workspace-pack-composition.false-closure.invalid.json", false],
|
|
353
|
+
["cross-workspace-pack-composition", "contracts/cross-workspace-pack-composition.authority-collapse.invalid.json", false],
|
|
354
|
+
["pack", "contracts/capability-pack.valid.json", true],
|
|
355
|
+
["pack", "contracts/capability-pack.missing-entry.invalid.json", false],
|
|
79
356
|
["profile", "contracts/effective-profile.valid.json", true],
|
|
357
|
+
["profile", "contracts/effective-profile.duplicate-rule.invalid.json", false],
|
|
80
358
|
["profile-candidate", "contracts/profile-change-candidate.valid.json", true],
|
|
359
|
+
["profile-candidate", "contracts/profile-change-candidate.unconfirmed.invalid.json", false],
|
|
81
360
|
["envelope", "contracts/task-envelope.valid.json", true],
|
|
82
361
|
["envelope", "contracts/task-envelope.overlap.invalid.json", false],
|
|
83
362
|
["launch", "contracts/launch-contract.valid.json", true],
|
|
363
|
+
["launch", "contracts/launch-contract.execution-owner.invalid.json", false],
|
|
84
364
|
["receipt", "contracts/return-receipt.valid.json", true],
|
|
85
365
|
["receipt", "contracts/return-receipt.deviation.invalid.json", false],
|
|
86
366
|
["host-adapter", "contracts/host-adapter.codex.valid.json", true],
|
|
87
367
|
["host-adapter", "contracts/host-adapter.claude-code.profile-only.valid.json", true],
|
|
88
368
|
["host-adapter", "contracts/host-adapter.cursor.profile-only.valid.json", true],
|
|
369
|
+
["host-adapter", "contracts/host-adapter.false-supported.invalid.json", false],
|
|
89
370
|
["adoption-plan", "contracts/adoption-plan.single-host.valid.json", true],
|
|
90
371
|
["adoption-plan", "contracts/adoption-plan.multi-host.valid.json", true],
|
|
91
372
|
["adoption-plan", "contracts/adoption-plan.mutating.invalid.json", false],
|
|
92
373
|
["binding-receipt", "contracts/host-binding-receipt.valid.json", true],
|
|
93
374
|
["binding-receipt", "contracts/host-binding-receipt.unverified.invalid.json", false],
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
function task29InvalidityWitness(caseId, observations) {
|
|
378
|
+
const digest = (value) => typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
|
|
379
|
+
if (caseId === "missing-identity") return typeof observations.requiredIdentity === "string" && observations.observedIdentity === null;
|
|
380
|
+
if (caseId === "stale-base-digest") return digest(observations.candidateBaseDigest) && digest(observations.observedTargetDigest) && observations.candidateBaseDigest !== observations.observedTargetDigest;
|
|
381
|
+
if (caseId === "forged-grant") return digest(observations.declaredGrantDigest) && digest(observations.expectedGrantDigest) && observations.declaredGrantDigest !== observations.expectedGrantDigest;
|
|
382
|
+
if (caseId === "ineligible-issuer") return Array.isArray(observations.eligibleIssuerSourceIds) && !observations.eligibleIssuerSourceIds.includes(observations.observedIssuerSourceId);
|
|
383
|
+
if (caseId === "duplicate-actor") return Number.isInteger(observations.minimumDistinctActors) && new Set(observations.observedActorIds ?? []).size < observations.minimumDistinctActors;
|
|
384
|
+
if (caseId === "revoked-approval") return observations.requiredLifecycle === "current" && observations.observedLifecycle === "revoked";
|
|
385
|
+
if (caseId === "wrong-trust") return Array.isArray(observations.acceptedTrustClasses) && !observations.acceptedTrustClasses.includes(observations.observedTrustClass);
|
|
386
|
+
if (caseId === "dangling-dependency") return Array.isArray(observations.declaredDependencyEdgeIds) && observations.declaredDependencyEdgeIds.some((id) => !(observations.availableDependencyEdgeIds ?? []).includes(id));
|
|
387
|
+
if (caseId === "changed-subject") return typeof observations.envelopeSubjectId === "string" && typeof observations.receiptSubjectId === "string" && observations.envelopeSubjectId !== observations.receiptSubjectId;
|
|
388
|
+
if (caseId === "cross-workspace-authority-collapse") return observations.authorityTransfer === true || observations.operationGrantTransfer === true || observations.receiptSubstitution === true;
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function validateTask29FixtureMatrix(fixtureRoot = resolve(root, "scripts/fixtures")) {
|
|
393
|
+
const errors = [];
|
|
394
|
+
const schemaCoverage = contractTypes.map((type) => ({
|
|
395
|
+
type,
|
|
396
|
+
valid: contractFixtureCases.some(([candidate, , expected]) => candidate === type && expected === true),
|
|
397
|
+
invalid: contractFixtureCases.some(([candidate, , expected]) => candidate === type && expected === false),
|
|
398
|
+
}));
|
|
399
|
+
for (const coverage of schemaCoverage) {
|
|
400
|
+
if (!coverage.valid) errors.push(`task 2.9 fixture coverage is missing a valid case for ${coverage.type}`);
|
|
401
|
+
if (!coverage.invalid) errors.push(`task 2.9 fixture coverage is missing an invalid case for ${coverage.type}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const matrixPath = resolve(fixtureRoot, "control-system/task-2.9-invalid-boundary-matrix.json");
|
|
405
|
+
const matrix = readJson(matrixPath);
|
|
406
|
+
const expectedCaseIds = [
|
|
407
|
+
"missing-identity", "stale-base-digest", "forged-grant", "ineligible-issuer", "duplicate-actor",
|
|
408
|
+
"revoked-approval", "wrong-trust", "dangling-dependency", "changed-subject", "cross-workspace-authority-collapse",
|
|
94
409
|
];
|
|
95
|
-
|
|
410
|
+
if (matrix.schemaVersion !== "1.0") errors.push("task 2.9 boundary matrix schemaVersion must equal 1.0");
|
|
411
|
+
if (matrix.matrixId !== "task-2.9-invalid-boundaries") errors.push("task 2.9 boundary matrix id is invalid");
|
|
412
|
+
if (matrix.evaluatorImplemented !== false) errors.push("task 2.9 boundary matrix must not claim an evaluator implementation");
|
|
413
|
+
if (!Array.isArray(matrix.cases)) errors.push("task 2.9 boundary matrix cases must be an array");
|
|
414
|
+
const caseIds = (matrix.cases ?? []).map((item) => item.caseId);
|
|
415
|
+
if (new Set(caseIds).size !== caseIds.length || !sameMembers(caseIds, expectedCaseIds)) errors.push("task 2.9 boundary matrix must cover each required invalid boundary exactly once");
|
|
416
|
+
|
|
417
|
+
const results = [];
|
|
418
|
+
for (const item of matrix.cases ?? []) {
|
|
419
|
+
const caseErrors = [];
|
|
420
|
+
if (!expectedCaseIds.includes(item.caseId)) caseErrors.push(`unknown case id: ${item.caseId}`);
|
|
421
|
+
if (!["schema", "cross-contract"].includes(item.validationLayer)) caseErrors.push("validationLayer must be schema or cross-contract");
|
|
422
|
+
if (item.expectedDisposition !== "rejected" || item.expectedReason !== item.caseId) caseErrors.push("expected rejection reason must match case id");
|
|
423
|
+
if (!item.observations || typeof item.observations !== "object" || Array.isArray(item.observations) || Object.keys(item.observations).length < 2) caseErrors.push("case must carry bounded comparison observations");
|
|
424
|
+
else if (!task29InvalidityWitness(item.caseId, item.observations)) caseErrors.push("bounded observations do not witness the declared invalid boundary");
|
|
425
|
+
if (!Array.isArray(item.contractFixtures) || item.contractFixtures.length === 0) caseErrors.push("case must reference at least one contract fixture");
|
|
426
|
+
const componentResults = [];
|
|
427
|
+
for (const component of item.contractFixtures ?? []) {
|
|
428
|
+
const componentPath = typeof component.path === "string" && /^contracts\/[a-z0-9.-]+\.json$/.test(component.path)
|
|
429
|
+
? resolve(fixtureRoot, component.path)
|
|
430
|
+
: null;
|
|
431
|
+
if (!contractTypes.includes(component.type)) caseErrors.push(`unknown contract type: ${component.type}`);
|
|
432
|
+
if (!componentPath || !existsSync(componentPath)) caseErrors.push(`missing contract fixture: ${component.path}`);
|
|
433
|
+
if (typeof component.expectedSchemaValid !== "boolean") caseErrors.push(`expectedSchemaValid must be boolean: ${component.path}`);
|
|
434
|
+
if (contractTypes.includes(component.type) && componentPath && existsSync(componentPath)) {
|
|
435
|
+
const validation = validateContract(component.type, readJson(componentPath));
|
|
436
|
+
componentResults.push({ type: component.type, path: component.path, expected: component.expectedSchemaValid, actual: validation.valid, errors: validation.errors });
|
|
437
|
+
if (validation.valid !== component.expectedSchemaValid) caseErrors.push(`contract fixture validity mismatch: ${component.path}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (item.validationLayer === "schema" && !(item.contractFixtures ?? []).some((component) => component.expectedSchemaValid === false)) caseErrors.push("schema-layer invalid case must contain an invalid contract fixture");
|
|
441
|
+
if (item.validationLayer === "cross-contract" && (item.contractFixtures ?? []).some((component) => component.expectedSchemaValid !== true)) caseErrors.push("cross-contract invalid case must keep every component schema-valid");
|
|
442
|
+
results.push({ caseId: item.caseId, validationLayer: item.validationLayer, passed: caseErrors.length === 0, errors: caseErrors, components: componentResults });
|
|
443
|
+
errors.push(...caseErrors.map((error) => `${item.caseId}: ${error}`));
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
passed: errors.length === 0,
|
|
447
|
+
errors,
|
|
448
|
+
schemaCoverage: {
|
|
449
|
+
total: schemaCoverage.length,
|
|
450
|
+
valid: schemaCoverage.filter((item) => item.valid).length,
|
|
451
|
+
invalid: schemaCoverage.filter((item) => item.invalid).length,
|
|
452
|
+
items: schemaCoverage,
|
|
453
|
+
},
|
|
454
|
+
evaluatorImplemented: false,
|
|
455
|
+
boundaryCases: results,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function validateTask4IncubationMatrix(fixtureRoot = resolve(root, "scripts/fixtures")) {
|
|
460
|
+
const errors = [];
|
|
461
|
+
const matrix = readJson(resolve(fixtureRoot, "control-system/task-4-diagnosis-health-separation.json"));
|
|
462
|
+
const allowedOwners = new Set(["diagnosis", "health", "core", "architecture-convergence"]);
|
|
463
|
+
const expectedOwners = new Map([
|
|
464
|
+
["nested-owner-boundary", "diagnosis"],
|
|
465
|
+
["nearest-governance-sources", "diagnosis"],
|
|
466
|
+
["dependency-unavailable", "diagnosis"],
|
|
467
|
+
["baseline-debt", "diagnosis"],
|
|
468
|
+
["stale-unclaimed-worktree", "health"],
|
|
469
|
+
["specification-adr-residue", "health"],
|
|
470
|
+
["ci-generated-drift", "health"],
|
|
471
|
+
["compatibility-test-abstraction-bloat", "health"],
|
|
472
|
+
["runner-qualification-stale", "health"],
|
|
473
|
+
["exact-authority-and-receipt", "core"],
|
|
474
|
+
["duplicate-capability-owner", "architecture-convergence"],
|
|
475
|
+
]);
|
|
476
|
+
if (matrix.schemaVersion !== "1.0") errors.push("task 4 matrix schemaVersion must equal 1.0");
|
|
477
|
+
if (matrix.matrixId !== "task-4-diagnosis-health-separation") errors.push("task 4 matrix id is invalid");
|
|
478
|
+
if (matrix.analyzeFirst !== true || matrix.mutationsAllowed !== false) errors.push("task 4 matrix must remain Analyze First and non-mutating");
|
|
479
|
+
const caseIds = (matrix.cases ?? []).map((item) => item.caseId);
|
|
480
|
+
if (caseIds.length !== expectedOwners.size || new Set(caseIds).size !== caseIds.length) errors.push("task 4 matrix must cover each owner-separation case exactly once");
|
|
481
|
+
for (const [caseId, owner] of expectedOwners) {
|
|
482
|
+
const item = (matrix.cases ?? []).find((candidate) => candidate.caseId === caseId);
|
|
483
|
+
if (!item) errors.push(`task 4 matrix is missing ${caseId}`);
|
|
484
|
+
else {
|
|
485
|
+
if (!allowedOwners.has(item.expectedOwner) || item.expectedOwner !== owner) errors.push(`${caseId}: expected owner must be ${owner}`);
|
|
486
|
+
if (!Array.isArray(item.observations) || item.observations.length === 0) errors.push(`${caseId}: bounded observations are required`);
|
|
487
|
+
if (typeof item.expectedOutput !== "string" || item.expectedOutput.length === 0) errors.push(`${caseId}: expected output is required`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return { passed: errors.length === 0, errors, cases: matrix.cases?.length ?? 0, mutationsAllowed: matrix.mutationsAllowed };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function validateArchitectureConvergenceMatrix(fixtureRoot = resolve(root, "scripts/fixtures")) {
|
|
494
|
+
const errors = [];
|
|
495
|
+
const matrix = readJson(resolve(fixtureRoot, "control-system/architecture-convergence-cases.json"));
|
|
496
|
+
const expectedActions = new Map([
|
|
497
|
+
["diagnosis-routes-source-discovery", "discover-sources"],
|
|
498
|
+
["health-routes-stale-worktrees", "assess-recurring-health"],
|
|
499
|
+
["core-routes-authority", "evaluate-deterministic-control"],
|
|
500
|
+
["thin-seam-retained", "retain"],
|
|
501
|
+
["large-cohesive-slice-retained", "retain"],
|
|
502
|
+
["second-identity-fence", "consolidate"],
|
|
503
|
+
["parallel-state-machine", "delete-duplicate"],
|
|
504
|
+
["duplicate-parser", "reuse-parser"],
|
|
505
|
+
["speculative-compatibility", "defer-or-delete"],
|
|
506
|
+
["recovery-multiplication", "consolidate"],
|
|
507
|
+
["implementation-shaped-tests", "delete-tests-preserve-invariants"],
|
|
508
|
+
["incomplete-evidence", "verify-before-deletion"],
|
|
509
|
+
["existing-profile-objects", "read-existing-objects"],
|
|
510
|
+
["issue-without-authorization", "return-template-only"],
|
|
511
|
+
]);
|
|
512
|
+
if (matrix.schemaVersion !== "1.0" || matrix.matrixId !== "architecture-convergence-audit") errors.push("architecture convergence matrix identity is invalid");
|
|
513
|
+
if (matrix.risk !== "medium" || matrix.analyzeFirst !== true || matrix.defaultMutation !== "read-only") errors.push("architecture convergence must be medium-risk Analyze First and read-only");
|
|
514
|
+
if (matrix.createsState !== false || matrix.requiresAuthentication !== false) errors.push("architecture convergence cannot create state or require authentication");
|
|
515
|
+
const ids = (matrix.cases ?? []).map((item) => item.caseId);
|
|
516
|
+
if (ids.length !== expectedActions.size || new Set(ids).size !== ids.length) errors.push("architecture convergence cases must be unique and complete");
|
|
517
|
+
for (const [caseId, action] of expectedActions) {
|
|
518
|
+
const item = (matrix.cases ?? []).find((candidate) => candidate.caseId === caseId);
|
|
519
|
+
if (!item) errors.push(`architecture convergence is missing ${caseId}`);
|
|
520
|
+
else {
|
|
521
|
+
if (item.expectedAction !== action) errors.push(`${caseId}: expected action must be ${action}`);
|
|
522
|
+
if (item.evidenceCompleteness === "incomplete" && item.expectedFinding !== "inferred-redundancy") errors.push(`${caseId}: incomplete evidence must remain inference`);
|
|
523
|
+
if (item.expectedAction.includes("delete") && item.evidenceCompleteness !== "complete") errors.push(`${caseId}: deletion requires complete evidence`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const pack = readJson(resolve(root, "packs/architecture-convergence-audit.json"));
|
|
527
|
+
const packValidation = validateContract("pack", pack);
|
|
528
|
+
if (!packValidation.valid) errors.push(...packValidation.errors);
|
|
529
|
+
if (pack.risk !== "medium" || pack.dependencies.length !== 0 || pack.approvals.length !== 0) errors.push("architecture convergence manifest boundary is invalid");
|
|
530
|
+
for (const path of [
|
|
531
|
+
"skills/architecture-convergence-audit/SKILL.md",
|
|
532
|
+
"skills/architecture-convergence-audit/references/audit-contract.md",
|
|
533
|
+
"skills/architecture-convergence-audit/assets/github-issue-template.md",
|
|
534
|
+
]) if (!existsSync(resolve(root, path))) errors.push(`architecture convergence asset is missing: ${path}`);
|
|
535
|
+
return { passed: errors.length === 0, errors, cases: matrix.cases?.length ?? 0, externalMutations: 0, persistedState: 0 };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function validateFixtures() {
|
|
539
|
+
const fixtureRoot = resolve(root, "scripts/fixtures");
|
|
540
|
+
const results = contractFixtureCases.map(([type, path, expected]) => {
|
|
96
541
|
const result = validateContract(type, readJson(resolve(fixtureRoot, path)));
|
|
97
542
|
return { type, path, expected, actual: result.valid, passed: result.valid === expected, errors: result.errors };
|
|
98
543
|
});
|
|
544
|
+
const task29Matrix = validateTask29FixtureMatrix(fixtureRoot);
|
|
545
|
+
results.push({ type: "fixture-matrix", path: "control-system/task-2.9-invalid-boundary-matrix.json", expected: true, actual: task29Matrix.passed, passed: task29Matrix.passed, errors: task29Matrix.errors, details: task29Matrix });
|
|
546
|
+
const task4Matrix = validateTask4IncubationMatrix(fixtureRoot);
|
|
547
|
+
results.push({ type: "fixture-matrix", path: "control-system/task-4-diagnosis-health-separation.json", expected: true, actual: task4Matrix.passed, passed: task4Matrix.passed, errors: task4Matrix.errors, details: task4Matrix });
|
|
548
|
+
const architectureConvergence = validateArchitectureConvergenceMatrix(fixtureRoot);
|
|
549
|
+
results.push({ type: "fixture-matrix", path: "control-system/architecture-convergence-cases.json", expected: true, actual: architectureConvergence.passed, passed: architectureConvergence.passed, errors: architectureConvergence.errors, details: architectureConvergence });
|
|
99
550
|
for (const workspace of ["markdown-existing", "empty-records"]) {
|
|
100
551
|
const path = resolve(fixtureRoot, "workspaces", workspace);
|
|
101
552
|
const before = JSON.stringify(workspaceSnapshot(path));
|
|
@@ -170,6 +621,8 @@ if (command === "validate") {
|
|
|
170
621
|
const result = validatePlugin(); emit(result); if (!result.valid) process.exitCode = 1;
|
|
171
622
|
} else if (command === "validate-fixtures") {
|
|
172
623
|
const result = validateFixtures(); emit(result); if (!result.passed) process.exitCode = 1;
|
|
624
|
+
} else if (command === "validate-fixture-matrix") {
|
|
625
|
+
const result = validateTask29FixtureMatrix(); emit(result); if (!result.passed) process.exitCode = 1;
|
|
173
626
|
} else if (command === "validate-adoption") {
|
|
174
627
|
const result = validateAdoption(); emit(result); if (!result.passed) process.exitCode = 1;
|
|
175
628
|
} else if (command === "validate-contract") {
|
|
@@ -207,4 +660,4 @@ if (command === "validate") {
|
|
|
207
660
|
const output = arg("--output"); if (!output) fail("build-bundle requires --output");
|
|
208
661
|
const result = buildBundle(root, output);
|
|
209
662
|
emit(process.argv.includes("--summary") ? { schemaVersion: result.schemaVersion, productId: result.productId, projection: result.projection, fileCount: result.fileCount, totalBytes: result.totalBytes, digest: result.digest, receiptDigest: result.receiptDigest } : result);
|
|
210
|
-
} else fail("usage: forgerail.mjs validate | validate-fixtures | validate-adoption | validate-contract | diagnose | adoption-plan | resolve-profile | launch | verify-receipt | build-bundle");
|
|
663
|
+
} else fail("usage: forgerail.mjs validate | validate-fixtures | validate-fixture-matrix | validate-adoption | validate-contract | diagnose | adoption-plan | resolve-profile | launch | verify-receipt | build-bundle");
|