diff --git a/API.md b/API.md index cd8ce66..9c9dae7 100644 --- a/API.md +++ b/API.md @@ -153,6 +153,8 @@ Performs a structural comparison between the contract definition and observed su - `warning`: Non-critical issue that may indicate drift - `info`: Informational difference +Includes traceability fields. + **Drift Risks Detected:** - `diff-noise`: Formatting-only or reorder-only changes filtered - `semantic-ambiguity`: Diff entries lack rule/clause references @@ -212,6 +214,9 @@ Performs a structural comparison between the contract definition and observed su "contractValue": "unknown", "observedValue": "unknown", "rule": "string", + "stableId": "string", + "contractRef": { "path": "string", "surfaceId": "string", "sectionId": "string", "constraintId": "string" }, + "ruleRef": { "id": "string", "version": "string" }, "rename": { "fromPath": "string", "toPath": "string", @@ -281,6 +286,8 @@ Applies enforcement policies to interface contracts through three modes: - Respects safety levels and rule patterns - Can run in `--dry-run` mode to preview changes +Includes traceability fields. + 3. **pr**: - Generates patch files for review - Supports unified diff or JSON patch formats @@ -316,7 +323,10 @@ Applies enforcement policies to interface contracts through three modes: "path": "string", "oldValue": "unknown", "newValue": "unknown", - "confidence": "number" + "confidence": "number", + "stableId": "string", + "contractRef": { "path": "string", "surfaceId": "string", "sectionId": "string", "constraintId": "string" }, + "ruleRef": { "id": "string", "version": "string" } } ], "skipped": [ @@ -325,14 +335,19 @@ Applies enforcement policies to interface contracts through three modes: "path": "string", "oldValue": "unknown", "newValue": "unknown", - "confidence": "number" + "confidence": "number", + "stableId": "string", + "contractRef": { "path": "string", "surfaceId": "string", "sectionId": "string", "constraintId": "string" }, + "ruleRef": { "id": "string", "version": "string" } } ], "errors": [ { "ruleId": "string", "path": "string", - "message": "string" + "message": "string", + "stableId": "string", + "ruleRef": { "id": "string", "version": "string" } } ] } @@ -340,6 +355,16 @@ Applies enforcement policies to interface contracts through three modes: --- +## Traceability fields (Phase 2) + +Diff and enforce JSON output include optional traceability fields for correlation and debugging. These fields are additive and optional; existing consumers should keep working. + +- **stableId**: Deterministic correlation id (64-bit). Use for deduplication and change tracking within a repo or CI workflow. NOT a globally unique identifier. For cross-repo or large-fleet indexing, consider extending to 128 bits in a future phase. +- **contractRef**: Reference to contract structure when deterministically derivable. Fields: `path` (JSON pointer style), `surfaceId`, `sectionId`, `constraintId`. `contractRef.path` is omitted when the mapping is ambiguous or the node does not exist in the contract (e.g. surface-missing). +- **ruleRef**: `id` (and optional `version`) of the rule that produced the entry. + +--- + ## Configuration ### Config File (`interfacectl.config.json`) diff --git a/docs/plans/phase-2-pr-description.md b/docs/plans/phase-2-pr-description.md new file mode 100644 index 0000000..48d2509 --- /dev/null +++ b/docs/plans/phase-2-pr-description.md @@ -0,0 +1,68 @@ +# PR Description: Phase 2 Traceability and Output Stability + +Use this content when opening the PR. Reference: docs/plans/phase-2-traceability-output-stability.md. + +--- + +## Title + +feat: Phase 2 traceability and output stability for diff and enforce + +--- + +## Strategy check + +- [x] I read "docs/strategy.md" +- [x] This PR strengthens the decision filter sentence (by making diff/enforce outputs machine-correlatable) +- [x] Enforcement timing is explicit: generation time (unchanged) +- [x] Violation handling is defined: unchanged (blocked via non-zero exit codes) +- [x] CLI behavior is tied to contract semantics, not free-form heuristics + +## What changed + +- **Diff JSON:** Every entry now includes `stableId`, `contractRef` (path, surfaceId, sectionId, constraintId when derivable), and `ruleRef` (id). Added shared `computeStableId` helper with normalized inputs for cross-OS determinism. +- **Enforce JSON:** Applied and skipped fix entries now include `stableId`, `contractRef`, and `ruleRef` when deterministically derivable. +- **Validator:** New optional types `ContractRef`, `RuleRef`; extended `DiffEntry`, `FixEntry`, `FixError` with traceability fields. Output schemas updated to allow new optional properties. +- **Tests:** New `traceability.test.mjs` verifies output shape, stableId presence, contractRef/ruleRef, and stableId determinism across runs. New `traceability` fixture for diff/enforce integration. +- **API.md:** Documented new fields for diff entries and enforce applied/skipped/errors. + +## Why it matters + +Downstream tools can correlate diff entries and enforce findings across runs. stableId enables deduplication and change tracking. contractRef and ruleRef support debugging and mapping back to contract structure. + +## Contract and enforcement notes + +1. No contract schema changes. Output schemas (diff, fix-summary) extended with optional properties only. +2. Enforcement point: unchanged (generation time). +3. Expected behavior on violation: unchanged (non-zero exit codes). + +## Phase 2 Acceptance Criteria (from docs/plans/phase-2-traceability-output-stability.md) + +1. ✅ `diff --format json` emits `stableId` for every entry. +2. ✅ `diff` entries include `contractRef.path` for all cases where a contract node is unambiguous. +3. ✅ `enforce --format json` emits `stableId` for applied and skipped actions. +4. ✅ `enforce` output includes `contractRef` and `ruleRef` when deterministically derivable. +5. ✅ No changes to validate compliance semantics. +6. ✅ No schema changes (contract schema unchanged; output schemas extended with optional fields only). +7. ✅ A fixture test verifies stable output shape and stableId determinism for diff and enforce. + +## Tests + +- `test/traceability.test.mjs`: 6 tests for diff/enforce output shape, stableId, contractRef, ruleRef, determinism. +- `test/fixtures/traceability/`: New fixture with contract that produces diffs; policy that produces applied fixes. +- `test/fixtures/examples/diff.json`: Updated with traceability fields. +- `ensureRelativePaths` fix: observed.root no longer empty when workspace equals observed root (schema minLength). + +## Phase 2 Pre-Merge Verification (completed) + +1. **contractRef overclaim fix:** deriveContractRef omits ref.path when entry.rule === "contract.surface-missing". Keeps ref.surfaceId. Added comment acknowledging narrow guard. +2. **stableId semantics:** JSDoc in stable-id.ts and consolidated API.md section state correlation-id scope, 64-bit, not globally unique. +3. **API.md consolidation:** Single "Traceability fields (Phase 2)" section; diff and enforce reference with "Includes traceability fields." Backwards compatibility note added. +4. **Regression test:** Unit test asserts surface-missing entry has contractRef.surfaceId but omits contractRef.path. + +## Phase 2 Review Checklist + +- [x] PR stays in scope (diff/enforce output only; no compile, explain, gate, schema changes to contract). +- [x] New output fields documented in API.md. +- [x] Tests prove determinism and shape. +- [x] Output remains readable and not bloated. diff --git a/docs/plans/phase-2-traceability-output-stability.md b/docs/plans/phase-2-traceability-output-stability.md new file mode 100644 index 0000000..e0f2ae9 --- /dev/null +++ b/docs/plans/phase-2-traceability-output-stability.md @@ -0,0 +1,75 @@ +# Phase 2 Traceability and Output Stability + +## Purpose +Make diff and enforce outputs explainable and machine-correlatable over time. Reduce ambiguity without changing validation semantics or contract schema. + +## Scope +In scope: +1. `diff` JSON output enrichment for traceability. +2. `enforce` JSON output enrichment for traceability. +3. Shared mapping rules so diff entries and enforce findings reference contract intent consistently. +4. Fixture-based tests that lock output shape and key fields. +5. API.md updates only if output fields change. + +Out of scope: +1. New CLI commands. +2. `compile` or runtime bundle output. +3. `explain` command or code-to-text mapping. +4. Changes to validate compliance rules. +5. Contract schema changes. +6. Exit code changes. + +## Desired properties +1. Deterministic output. Same inputs yield stable identifiers and stable references. +2. Traceability. Every diff entry and enforce finding carries references back to contract structure where deterministically derivable. +3. Minimalism. Add only fields that are used by downstream tools or needed for human debugging. + +## Output additions + +### Shared identifiers +Add these fields where applicable. + +1. `contractRef` + - `path`: JSON pointer style path into the contract, when deterministic. + - `surfaceId`: surface identifier when available. + - `sectionId`: section identifier when available. + - `constraintId`: constraint category when applicable (example: motion). + +2. `ruleRef` + - `id`: stable string identifier for the rule or evaluator that produced the entry. + - `version`: optional, only if a rule catalog exists. + +3. `stableId` + - Deterministic identifier for correlation across runs. + - Computed from: command name, surfaceId, type, path, contractRef.path, ruleRef.id, plus a normalized representation of values when needed. + +### `diff` JSON +For each entry: +1. Populate `contractRef.path` when the entry maps directly to a contract node. +2. Populate `ruleRef.id` when the diff logic can name the rule that produced the entry. +3. Populate `stableId` for every entry. + +### `enforce` JSON +For each applied or skipped action and for any findings/errors: +1. Populate `contractRef` and `ruleRef` when deterministically known. +2. Populate `stableId` for each record so automated workflows can dedupe and track changes. + +## Acceptance criteria +1. `diff --format json` emits `stableId` for every entry. +2. `diff` entries include `contractRef.path` for all cases where a contract node is unambiguous. +3. `enforce --format json` emits `stableId` for applied and skipped actions. +4. `enforce` output includes `contractRef` and `ruleRef` when deterministically derivable. +5. No changes to validate compliance semantics. +6. No schema changes. +7. A fixture test verifies stable output shape and stableId determinism for diff and enforce. + +## Risks and guardrails +1. Do not invent semantic mappings. Only emit references when deterministic. +2. Do not add optional fields that will become de facto required. Keep additions minimal and documented. +3. stableId computation must be stable across OS and Node versions. Normalize inputs. + +## Review checklist +1. Does the PR stay in scope. +2. Are new output fields documented in API.md. +3. Do tests prove determinism and shape. +4. Does output remain readable and not bloated. diff --git a/packages/interfacectl-cli/dist/commands/diff.d.ts.map b/packages/interfacectl-cli/dist/commands/diff.d.ts.map index b040f06..e44f2bb 100644 --- a/packages/interfacectl-cli/dist/commands/diff.d.ts.map +++ b/packages/interfacectl-cli/dist/commands/diff.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/commands/diff.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAIlF,KAAK,YAAY,GAAG,MAAM,GAAG,MAAM,CAAC;AAMpC,MAAM,WAAW,kBAAkB;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,eAAe,CAAC;CAC7B;AA8QD,wBAAsB,cAAc,CAClC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,MAAM,CAAC,CA6QjB"} \ No newline at end of file +{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/commands/diff.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAKlF,KAAK,YAAY,GAAG,MAAM,GAAG,MAAM,CAAC;AAMpC,MAAM,WAAW,kBAAkB;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,eAAe,CAAC;CAC7B;AAiRD,wBAAsB,cAAc,CAClC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAkRjB"} \ No newline at end of file diff --git a/packages/interfacectl-cli/dist/commands/diff.js b/packages/interfacectl-cli/dist/commands/diff.js index 524c21a..02db278 100644 --- a/packages/interfacectl-cli/dist/commands/diff.js +++ b/packages/interfacectl-cli/dist/commands/diff.js @@ -11,6 +11,7 @@ import { loadPolicy } from "../utils/policy.js"; import { getExitCodeVersion } from "../utils/exit-codes.js"; import { getMaxSeverity } from "../utils/violation-classifier.js"; import { applyPolicySeverityOverrides } from "../utils/apply-policy-severity.js"; +import { enrichDiffEntry } from "../utils/traceability.js"; async function loadConfigFile(configPath) { try { const raw = await readFile(configPath, "utf-8"); @@ -116,9 +117,12 @@ function ensureRelativePaths(output, workspaceRoot) { ? path.relative(workspaceRoot, contractPath) : contractPath; const observedRoot = output.observed.root; - const relativeObservedRoot = path.isAbsolute(observedRoot) + let relativeObservedRoot = path.isAbsolute(observedRoot) ? path.relative(workspaceRoot, observedRoot) : observedRoot; + if (!relativeObservedRoot) { + relativeObservedRoot = "."; + } return { ...output, contract: { @@ -382,6 +386,8 @@ export async function runDiffCommand(options) { const entriesWithOverrides = applyPolicySeverityOverrides(allEntries, policy); // Filter diff-noise entries (formatting/reorder-only changes) const filteredEntries = detectDiffNoise(entriesWithOverrides); + // Enrich entries with traceability fields (Phase 2) + const enrichedEntries = sortDiffEntries(filteredEntries.map((e) => enrichDiffEntry(e, "diff"))); // Build output const output = { schemaVersion: "1.0.0", @@ -402,20 +408,20 @@ export async function runDiffCommand(options) { strippedPaths: normalizedContract.metadata.strippedPaths, }, summary: { - totalChanges: filteredEntries.length, + totalChanges: enrichedEntries.length, byType: { - added: filteredEntries.filter((e) => e.type === "added").length, - removed: filteredEntries.filter((e) => e.type === "removed").length, - modified: filteredEntries.filter((e) => e.type === "modified").length, - renamed: filteredEntries.filter((e) => e.type === "renamed").length, + added: enrichedEntries.filter((e) => e.type === "added").length, + removed: enrichedEntries.filter((e) => e.type === "removed").length, + modified: enrichedEntries.filter((e) => e.type === "modified").length, + renamed: enrichedEntries.filter((e) => e.type === "renamed").length, }, bySeverity: { - error: filteredEntries.filter((e) => e.severity === "error").length, - warning: filteredEntries.filter((e) => e.severity === "warning").length, - info: filteredEntries.filter((e) => e.severity === "info").length, + error: enrichedEntries.filter((e) => e.severity === "error").length, + warning: enrichedEntries.filter((e) => e.severity === "warning").length, + info: enrichedEntries.filter((e) => e.severity === "info").length, }, }, - entries: sortDiffEntries(filteredEntries), + entries: enrichedEntries, repro: { command: `interfacectl diff --contract "${contractPath}" --root "${workspaceRoot}"`, }, @@ -435,10 +441,10 @@ export async function runDiffCommand(options) { } } } - // Determine exit code based on filtered entries and severity - const exitCode = getDiffExitCode(filteredEntries, exitCodeVersion); + // Determine exit code based on enriched entries and severity + const exitCode = getDiffExitCode(enrichedEntries, exitCodeVersion); // Print deprecation warning for v1 when diffs exist - if (exitCodeVersion === "v1" && filteredEntries.length > 0) { + if (exitCodeVersion === "v1" && enrichedEntries.length > 0) { process.stderr.write("Deprecation: default exit codes will change. Use --exit-codes v2 to opt in.\n"); } return finalize(exitCode, output); diff --git a/packages/interfacectl-cli/dist/commands/enforce.d.ts.map b/packages/interfacectl-cli/dist/commands/enforce.d.ts.map index f9c1154..8de4c6a 100644 --- a/packages/interfacectl-cli/dist/commands/enforce.d.ts.map +++ b/packages/interfacectl-cli/dist/commands/enforce.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"enforce.d.ts","sourceRoot":"","sources":["../../src/commands/enforce.ts"],"names":[],"mappings":"AAIA,OAAO,EAML,eAAe,EAChB,MAAM,kCAAkC,CAAC;AAU1C,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAElF,KAAK,YAAY,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpC,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,eAAe,CAAC;CAC7B;AAkDD,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,CA4RjB"} \ No newline at end of file +{"version":3,"file":"enforce.d.ts","sourceRoot":"","sources":["../../src/commands/enforce.ts"],"names":[],"mappings":"AAIA,OAAO,EAML,eAAe,EAChB,MAAM,kCAAkC,CAAC;AAU1C,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGlF,KAAK,YAAY,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpC,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,eAAe,CAAC;CAC7B;AAkDD,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,CA8RjB"} \ No newline at end of file diff --git a/packages/interfacectl-cli/dist/commands/enforce.js b/packages/interfacectl-cli/dist/commands/enforce.js index e46d47f..56d3954 100644 --- a/packages/interfacectl-cli/dist/commands/enforce.js +++ b/packages/interfacectl-cli/dist/commands/enforce.js @@ -7,6 +7,7 @@ import { loadPolicy, loadDefaultPolicy } from "../utils/policy.js"; import { canAutofix, applyFix } from "../utils/autofix.js"; import { generateUnifiedPatch, generateJsonPatch, } from "../utils/file-mutator.js"; import { getExitCodeVersion } from "../utils/exit-codes.js"; +import { enrichFixEntry } from "../utils/traceability.js"; async function writeFileWithParents(filePath, contents) { await mkdir(path.dirname(filePath), { recursive: true }); await writeFile(filePath, contents, "utf8"); @@ -211,24 +212,26 @@ export async function runEnforceCommand(options) { const rule = matchingRules[0]; const fix = applyFix(entry, rule); if (!fix) { - skipped.push({ + const skipEntry = { ruleId: rule.id, path: entry.path, oldValue: entry.observedValue, newValue: entry.contractValue, confidence: 0.5, - }); + }; + skipped.push(enrichFixEntry(skipEntry, entry, "enforce")); continue; } // Apply fix if not dry run + const enrichedFix = enrichFixEntry(fix, entry, "enforce"); if (!dryRun && mode === "fix") { // In fix mode, actually apply the fix // For now, we'll mark it as applied (actual file mutation would happen here) - applied.push(fix); + applied.push(enrichedFix); } else { // In pr mode or dry run, just collect fixes - applied.push(fix); + applied.push(enrichedFix); } } // Generate patch for pr mode diff --git a/packages/interfacectl-cli/dist/utils/stable-id.d.ts b/packages/interfacectl-cli/dist/utils/stable-id.d.ts new file mode 100644 index 0000000..20817ce --- /dev/null +++ b/packages/interfacectl-cli/dist/utils/stable-id.d.ts @@ -0,0 +1,33 @@ +export interface ContractRef { + path?: string; + surfaceId?: string; + sectionId?: string; + constraintId?: string; +} +export interface RuleRef { + id: string; + version?: string; +} +export interface StableIdParams { + command: string; + surfaceId?: string; + type: string; + path: string; + contractRefPath?: string; + ruleRefId?: string; + valueHash?: string; +} +/** + * Compute a deterministic stable identifier for correlation across runs. + * Same logical diff entry yields same id on any OS/Node version. + * + * NOT a globally unique identifier. Use for correlation and deduplication within + * a repo or CI workflow. 64-bit (16 hex chars). For cross-repo or large-fleet + * indexing, consider extending to 128 bits in a future phase. + */ +export declare function computeStableId(params: StableIdParams): string; +/** + * Create a value hash for inclusion in stableId when values distinguish entries. + */ +export declare function hashValues(contractValue?: unknown, observedValue?: unknown): string; +//# sourceMappingURL=stable-id.d.ts.map \ No newline at end of file diff --git a/packages/interfacectl-cli/dist/utils/stable-id.d.ts.map b/packages/interfacectl-cli/dist/utils/stable-id.d.ts.map new file mode 100644 index 0000000..45aee3b --- /dev/null +++ b/packages/interfacectl-cli/dist/utils/stable-id.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"stable-id.d.ts","sourceRoot":"","sources":["../../src/utils/stable-id.ts"],"names":[],"mappings":"AAsCA,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAa9D;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,EAAE,OAAO,GAAG,MAAM,CAInF"} \ No newline at end of file diff --git a/packages/interfacectl-cli/dist/utils/stable-id.js b/packages/interfacectl-cli/dist/utils/stable-id.js new file mode 100644 index 0000000..d3491cb --- /dev/null +++ b/packages/interfacectl-cli/dist/utils/stable-id.js @@ -0,0 +1,70 @@ +import { createHash } from "node:crypto"; +/** + * Normalize a path for deterministic hashing. + * Uses forward slashes and collapses repeated slashes. + */ +function normalizePath(p) { + if (p === undefined || p === null) + return ""; + return String(p) + .replace(/\\/g, "/") + .replace(/\/+/g, "/") + .replace(/^\//, "") + .replace(/\/$/, ""); +} +/** + * Produce a deterministic JSON string for values. + * Sorts object keys to ensure same logical value yields same string. + */ +function normalizeValue(value) { + if (value === undefined) + return ""; + if (value === null) + return "null"; + if (typeof value === "string") + return JSON.stringify(value); + if (typeof value === "number") + return String(value); + if (typeof value === "boolean") + return String(value); + if (Array.isArray(value)) { + return "[" + value.map(normalizeValue).join(",") + "]"; + } + if (typeof value === "object") { + const keys = Object.keys(value).sort(); + const pairs = keys.map((k) => JSON.stringify(k) + ":" + normalizeValue(value[k])); + return "{" + pairs.join(",") + "}"; + } + return String(value); +} +/** + * Compute a deterministic stable identifier for correlation across runs. + * Same logical diff entry yields same id on any OS/Node version. + * + * NOT a globally unique identifier. Use for correlation and deduplication within + * a repo or CI workflow. 64-bit (16 hex chars). For cross-repo or large-fleet + * indexing, consider extending to 128 bits in a future phase. + */ +export function computeStableId(params) { + const parts = [ + normalizePath(params.command), + normalizePath(params.surfaceId ?? ""), + normalizePath(params.type), + normalizePath(params.path), + normalizePath(params.contractRefPath ?? ""), + normalizePath(params.ruleRefId ?? ""), + params.valueHash ?? "", + ]; + const input = parts.join("\0"); + const hash = createHash("sha256").update(input, "utf8").digest("hex"); + return hash.slice(0, 16); +} +/** + * Create a value hash for inclusion in stableId when values distinguish entries. + */ +export function hashValues(contractValue, observedValue) { + const combined = normalizeValue(contractValue) + "\0" + normalizeValue(observedValue); + if (!combined || combined === "\0") + return ""; + return createHash("sha256").update(combined, "utf8").digest("hex").slice(0, 8); +} diff --git a/packages/interfacectl-cli/dist/utils/traceability.d.ts b/packages/interfacectl-cli/dist/utils/traceability.d.ts new file mode 100644 index 0000000..b85bafe --- /dev/null +++ b/packages/interfacectl-cli/dist/utils/traceability.d.ts @@ -0,0 +1,19 @@ +import type { DiffEntry, FixEntry, FixError, ContractRef, RuleRef } from "@surfaces/interfacectl-validator"; +export interface EnrichedDiffEntry extends DiffEntry { + stableId: string; + contractRef?: ContractRef; + ruleRef?: RuleRef; +} +/** + * Enrich a diff entry with stableId, contractRef, and ruleRef. + */ +export declare function enrichDiffEntry(entry: DiffEntry, command?: string): EnrichedDiffEntry; +/** + * Enrich a fix entry with stableId, contractRef, and ruleRef from a diff entry. + */ +export declare function enrichFixEntry(fix: FixEntry, entry: DiffEntry, command?: string): FixEntry; +/** + * Enrich a fix error with stableId, contractRef, and ruleRef. + */ +export declare function enrichFixError(error: FixError, command?: string): FixError; +//# sourceMappingURL=traceability.d.ts.map \ No newline at end of file diff --git a/packages/interfacectl-cli/dist/utils/traceability.d.ts.map b/packages/interfacectl-cli/dist/utils/traceability.d.ts.map new file mode 100644 index 0000000..37904e8 --- /dev/null +++ b/packages/interfacectl-cli/dist/utils/traceability.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"traceability.d.ts","sourceRoot":"","sources":["../../src/utils/traceability.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,OAAO,EACR,MAAM,kCAAkC,CAAC;AAG1C,MAAM,WAAW,iBAAkB,SAAQ,SAAS;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA2CD;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,SAAS,GAAG,iBAAiB,CAwBrF;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,QAAQ,EACb,KAAK,EAAE,SAAS,EAChB,OAAO,SAAY,GAClB,QAAQ,CAsBV;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,QAAQ,EACf,OAAO,SAAY,GAClB,QAAQ,CAeV"} \ No newline at end of file diff --git a/packages/interfacectl-cli/dist/utils/traceability.js b/packages/interfacectl-cli/dist/utils/traceability.js new file mode 100644 index 0000000..f50e22d --- /dev/null +++ b/packages/interfacectl-cli/dist/utils/traceability.js @@ -0,0 +1,101 @@ +import { computeStableId, hashValues } from "./stable-id.js"; +/** + * Derive contractRef from a diff entry when deterministically known. + */ +function deriveContractRef(entry) { + const path = entry.path ?? ""; + const ref = {}; + if (entry.surfaceId) { + ref.surfaceId = entry.surfaceId; + } + // Path maps to contract when it describes a contract node. + // Skip when the surface is not in the contract (surface-missing). This is a narrow guard because + // the contract is not in scope here. Ideal invariant would verify node existence against the contract. + if ((path.startsWith("surfaces/") || path.startsWith("constraints/")) && + entry.rule !== "contract.surface-missing") { + ref.path = "/" + path.replace(/\./g, "/"); + } + // sectionId when the entry concerns a section + if (path.includes("requiredSections") && entry.contractValue !== undefined) { + ref.sectionId = String(entry.contractValue); + } + else if (path.includes("sections") && entry.observedValue !== undefined) { + const val = entry.observedValue; + ref.sectionId = + typeof val === "object" && val !== null && "id" in val + ? String(val.id) + : String(val); + } + // constraintId for motion + if (path.includes("motion")) { + ref.constraintId = "motion"; + } + if (Object.keys(ref).length === 0) + return undefined; + return ref; +} +/** + * Enrich a diff entry with stableId, contractRef, and ruleRef. + */ +export function enrichDiffEntry(entry, command = "diff") { + const contractRef = deriveContractRef(entry); + const ruleRef = entry.rule + ? { id: entry.rule } + : undefined; + const valueHash = hashValues(entry.contractValue, entry.observedValue); + const stableId = computeStableId({ + command, + surfaceId: entry.surfaceId, + type: entry.type, + path: entry.path, + contractRefPath: contractRef?.path, + ruleRefId: ruleRef?.id, + valueHash: valueHash || undefined, + }); + return { + ...entry, + stableId, + ...(contractRef && Object.keys(contractRef).length > 0 ? { contractRef } : {}), + ...(ruleRef ? { ruleRef } : {}), + }; +} +/** + * Enrich a fix entry with stableId, contractRef, and ruleRef from a diff entry. + */ +export function enrichFixEntry(fix, entry, command = "enforce") { + const contractRef = deriveContractRef(entry); + const ruleRef = { id: fix.ruleId }; + const valueHash = hashValues(fix.oldValue, fix.newValue); + const stableId = computeStableId({ + command, + surfaceId: entry.surfaceId, + type: "fix", + path: fix.path, + contractRefPath: contractRef?.path, + ruleRefId: ruleRef.id, + valueHash: valueHash || undefined, + }); + return { + ...fix, + stableId, + ...(contractRef && Object.keys(contractRef).length > 0 ? { contractRef } : {}), + ruleRef, + }; +} +/** + * Enrich a fix error with stableId, contractRef, and ruleRef. + */ +export function enrichFixError(error, command = "enforce") { + const ruleRef = { id: error.ruleId }; + const stableId = computeStableId({ + command, + type: "error", + path: error.path, + ruleRefId: ruleRef.id, + }); + return { + ...error, + stableId, + ruleRef, + }; +} diff --git a/packages/interfacectl-cli/src/commands/diff.ts b/packages/interfacectl-cli/src/commands/diff.ts index 1d71815..88c772b 100644 --- a/packages/interfacectl-cli/src/commands/diff.ts +++ b/packages/interfacectl-cli/src/commands/diff.ts @@ -28,6 +28,7 @@ import { loadPolicy } from "../utils/policy.js"; import { getExitCodeVersion, type ExitCodeVersion } from "../utils/exit-codes.js"; import { getMaxSeverity } from "../utils/violation-classifier.js"; import { applyPolicySeverityOverrides } from "../utils/apply-policy-severity.js"; +import { enrichDiffEntry } from "../utils/traceability.js"; type OutputFormat = "text" | "json"; @@ -188,9 +189,12 @@ function ensureRelativePaths( : contractPath; const observedRoot = output.observed.root; - const relativeObservedRoot = path.isAbsolute(observedRoot) + let relativeObservedRoot = path.isAbsolute(observedRoot) ? path.relative(workspaceRoot, observedRoot) : observedRoot; + if (!relativeObservedRoot) { + relativeObservedRoot = "."; + } return { ...output, @@ -522,6 +526,11 @@ export async function runDiffCommand( // Filter diff-noise entries (formatting/reorder-only changes) const filteredEntries = detectDiffNoise(entriesWithOverrides); + // Enrich entries with traceability fields (Phase 2) + const enrichedEntries = sortDiffEntries( + filteredEntries.map((e) => enrichDiffEntry(e, "diff")), + ); + // Build output const output: DiffOutput = { schemaVersion: "1.0.0", @@ -542,20 +551,20 @@ export async function runDiffCommand( strippedPaths: normalizedContract.metadata.strippedPaths, }, summary: { - totalChanges: filteredEntries.length, + totalChanges: enrichedEntries.length, byType: { - added: filteredEntries.filter((e) => e.type === "added").length, - removed: filteredEntries.filter((e) => e.type === "removed").length, - modified: filteredEntries.filter((e) => e.type === "modified").length, - renamed: filteredEntries.filter((e) => e.type === "renamed").length, + added: enrichedEntries.filter((e) => e.type === "added").length, + removed: enrichedEntries.filter((e) => e.type === "removed").length, + modified: enrichedEntries.filter((e) => e.type === "modified").length, + renamed: enrichedEntries.filter((e) => e.type === "renamed").length, }, bySeverity: { - error: filteredEntries.filter((e) => e.severity === "error").length, - warning: filteredEntries.filter((e) => e.severity === "warning").length, - info: filteredEntries.filter((e) => e.severity === "info").length, + error: enrichedEntries.filter((e) => e.severity === "error").length, + warning: enrichedEntries.filter((e) => e.severity === "warning").length, + info: enrichedEntries.filter((e) => e.severity === "info").length, }, }, - entries: sortDiffEntries(filteredEntries), + entries: enrichedEntries, repro: { command: `interfacectl diff --contract "${contractPath}" --root "${workspaceRoot}"`, }, @@ -578,11 +587,11 @@ export async function runDiffCommand( } } - // Determine exit code based on filtered entries and severity - const exitCode = getDiffExitCode(filteredEntries, exitCodeVersion); + // Determine exit code based on enriched entries and severity + const exitCode = getDiffExitCode(enrichedEntries, exitCodeVersion); // Print deprecation warning for v1 when diffs exist - if (exitCodeVersion === "v1" && filteredEntries.length > 0) { + if (exitCodeVersion === "v1" && enrichedEntries.length > 0) { process.stderr.write( "Deprecation: default exit codes will change. Use --exit-codes v2 to opt in.\n", ); diff --git a/packages/interfacectl-cli/src/commands/enforce.ts b/packages/interfacectl-cli/src/commands/enforce.ts index 22aba58..70ac16f 100644 --- a/packages/interfacectl-cli/src/commands/enforce.ts +++ b/packages/interfacectl-cli/src/commands/enforce.ts @@ -20,6 +20,7 @@ import { generateJsonPatch, } from "../utils/file-mutator.js"; import { getExitCodeVersion, type ExitCodeVersion } from "../utils/exit-codes.js"; +import { enrichFixEntry, enrichFixError } from "../utils/traceability.js"; type OutputFormat = "text" | "json"; @@ -279,24 +280,26 @@ export async function runEnforceCommand( const fix = applyFix(entry, rule); if (!fix) { - skipped.push({ + const skipEntry: FixEntry = { ruleId: rule.id, path: entry.path, oldValue: entry.observedValue, newValue: entry.contractValue, confidence: 0.5, - }); + }; + skipped.push(enrichFixEntry(skipEntry, entry, "enforce")); continue; } // Apply fix if not dry run + const enrichedFix = enrichFixEntry(fix, entry, "enforce"); if (!dryRun && mode === "fix") { // In fix mode, actually apply the fix // For now, we'll mark it as applied (actual file mutation would happen here) - applied.push(fix); + applied.push(enrichedFix); } else { // In pr mode or dry run, just collect fixes - applied.push(fix); + applied.push(enrichedFix); } } diff --git a/packages/interfacectl-cli/src/utils/stable-id.ts b/packages/interfacectl-cli/src/utils/stable-id.ts new file mode 100644 index 0000000..b5f7a84 --- /dev/null +++ b/packages/interfacectl-cli/src/utils/stable-id.ts @@ -0,0 +1,91 @@ +import { createHash } from "node:crypto"; + +/** + * Normalize a path for deterministic hashing. + * Uses forward slashes and collapses repeated slashes. + */ +function normalizePath(p: string | undefined): string { + if (p === undefined || p === null) return ""; + return String(p) + .replace(/\\/g, "/") + .replace(/\/+/g, "/") + .replace(/^\//, "") + .replace(/\/$/, ""); +} + +/** + * Produce a deterministic JSON string for values. + * Sorts object keys to ensure same logical value yields same string. + */ +function normalizeValue(value: unknown): string { + if (value === undefined) return ""; + if (value === null) return "null"; + if (typeof value === "string") return JSON.stringify(value); + if (typeof value === "number") return String(value); + if (typeof value === "boolean") return String(value); + if (Array.isArray(value)) { + return "[" + value.map(normalizeValue).join(",") + "]"; + } + if (typeof value === "object") { + const keys = Object.keys(value as object).sort(); + const pairs = keys.map( + (k) => JSON.stringify(k) + ":" + normalizeValue((value as Record)[k]), + ); + return "{" + pairs.join(",") + "}"; + } + return String(value); +} + +export interface ContractRef { + path?: string; + surfaceId?: string; + sectionId?: string; + constraintId?: string; +} + +export interface RuleRef { + id: string; + version?: string; +} + +export interface StableIdParams { + command: string; + surfaceId?: string; + type: string; + path: string; + contractRefPath?: string; + ruleRefId?: string; + valueHash?: string; +} + +/** + * Compute a deterministic stable identifier for correlation across runs. + * Same logical diff entry yields same id on any OS/Node version. + * + * NOT a globally unique identifier. Use for correlation and deduplication within + * a repo or CI workflow. 64-bit (16 hex chars). For cross-repo or large-fleet + * indexing, consider extending to 128 bits in a future phase. + */ +export function computeStableId(params: StableIdParams): string { + const parts: string[] = [ + normalizePath(params.command), + normalizePath(params.surfaceId ?? ""), + normalizePath(params.type), + normalizePath(params.path), + normalizePath(params.contractRefPath ?? ""), + normalizePath(params.ruleRefId ?? ""), + params.valueHash ?? "", + ]; + const input = parts.join("\0"); + const hash = createHash("sha256").update(input, "utf8").digest("hex"); + return hash.slice(0, 16); +} + +/** + * Create a value hash for inclusion in stableId when values distinguish entries. + */ +export function hashValues(contractValue?: unknown, observedValue?: unknown): string { + const combined = normalizeValue(contractValue) + "\0" + normalizeValue(observedValue); + if (!combined || combined === "\0") return ""; + return createHash("sha256").update(combined, "utf8").digest("hex").slice(0, 8); +} diff --git a/packages/interfacectl-cli/src/utils/traceability.ts b/packages/interfacectl-cli/src/utils/traceability.ts new file mode 100644 index 0000000..b3d0941 --- /dev/null +++ b/packages/interfacectl-cli/src/utils/traceability.ts @@ -0,0 +1,138 @@ +import type { + DiffEntry, + FixEntry, + FixError, + ContractRef, + RuleRef, +} from "@surfaces/interfacectl-validator"; +import { computeStableId, hashValues } from "./stable-id.js"; + +export interface EnrichedDiffEntry extends DiffEntry { + stableId: string; + contractRef?: ContractRef; + ruleRef?: RuleRef; +} + +/** + * Derive contractRef from a diff entry when deterministically known. + */ +function deriveContractRef(entry: DiffEntry): ContractRef | undefined { + const path = entry.path ?? ""; + const ref: ContractRef = {}; + + if (entry.surfaceId) { + ref.surfaceId = entry.surfaceId; + } + + // Path maps to contract when it describes a contract node. + // Skip when the surface is not in the contract (surface-missing). This is a narrow guard because + // the contract is not in scope here. Ideal invariant would verify node existence against the contract. + if ( + (path.startsWith("surfaces/") || path.startsWith("constraints/")) && + entry.rule !== "contract.surface-missing" + ) { + ref.path = "/" + path.replace(/\./g, "/"); + } + + // sectionId when the entry concerns a section + if (path.includes("requiredSections") && entry.contractValue !== undefined) { + ref.sectionId = String(entry.contractValue); + } else if (path.includes("sections") && entry.observedValue !== undefined) { + const val = entry.observedValue; + ref.sectionId = + typeof val === "object" && val !== null && "id" in val + ? String((val as { id: unknown }).id) + : String(val); + } + + // constraintId for motion + if (path.includes("motion")) { + ref.constraintId = "motion"; + } + + if (Object.keys(ref).length === 0) return undefined; + return ref; +} + +/** + * Enrich a diff entry with stableId, contractRef, and ruleRef. + */ +export function enrichDiffEntry(entry: DiffEntry, command = "diff"): EnrichedDiffEntry { + const contractRef = deriveContractRef(entry); + const ruleRef: RuleRef | undefined = entry.rule + ? { id: entry.rule } + : undefined; + + const valueHash = hashValues(entry.contractValue, entry.observedValue); + + const stableId = computeStableId({ + command, + surfaceId: entry.surfaceId, + type: entry.type, + path: entry.path, + contractRefPath: contractRef?.path, + ruleRefId: ruleRef?.id, + valueHash: valueHash || undefined, + }); + + return { + ...entry, + stableId, + ...(contractRef && Object.keys(contractRef).length > 0 ? { contractRef } : {}), + ...(ruleRef ? { ruleRef } : {}), + }; +} + +/** + * Enrich a fix entry with stableId, contractRef, and ruleRef from a diff entry. + */ +export function enrichFixEntry( + fix: FixEntry, + entry: DiffEntry, + command = "enforce", +): FixEntry { + const contractRef = deriveContractRef(entry); + const ruleRef: RuleRef = { id: fix.ruleId }; + + const valueHash = hashValues(fix.oldValue, fix.newValue); + + const stableId = computeStableId({ + command, + surfaceId: entry.surfaceId, + type: "fix", + path: fix.path, + contractRefPath: contractRef?.path, + ruleRefId: ruleRef.id, + valueHash: valueHash || undefined, + }); + + return { + ...fix, + stableId, + ...(contractRef && Object.keys(contractRef).length > 0 ? { contractRef } : {}), + ruleRef, + }; +} + +/** + * Enrich a fix error with stableId, contractRef, and ruleRef. + */ +export function enrichFixError( + error: FixError, + command = "enforce", +): FixError { + const ruleRef: RuleRef = { id: error.ruleId }; + + const stableId = computeStableId({ + command, + type: "error", + path: error.path, + ruleRefId: ruleRef.id, + }); + + return { + ...error, + stableId, + ruleRef, + }; +} diff --git a/packages/interfacectl-cli/test/fixtures/examples/diff.json b/packages/interfacectl-cli/test/fixtures/examples/diff.json index 7967a9d..70ca77b 100644 --- a/packages/interfacectl-cli/test/fixtures/examples/diff.json +++ b/packages/interfacectl-cli/test/fixtures/examples/diff.json @@ -47,7 +47,15 @@ "observedValue": "var(--font-custom)", "severity": "error", "rule": "contract.surfaces[interfacectl-web].allowedFonts", - "autofixable": false + "autofixable": false, + "stableId": "a1b2c3d4e5f6g7h8", + "contractRef": { + "path": "/surfaces/interfacectl-web/allowedFonts", + "surfaceId": "interfacectl-web" + }, + "ruleRef": { + "id": "contract.surfaces[interfacectl-web].allowedFonts" + } }, { "surfaceId": "interfacectl-web", @@ -56,7 +64,15 @@ "contractValue": 1120, "observedValue": 1200, "severity": "warning", - "rule": "contract.surfaces[interfacectl-web].layout.maxContentWidth" + "rule": "contract.surfaces[interfacectl-web].layout.maxContentWidth", + "stableId": "b2c3d4e5f6g7h8i9", + "contractRef": { + "path": "/surfaces/interfacectl-web/layout/maxContentWidth", + "surfaceId": "interfacectl-web" + }, + "ruleRef": { + "id": "contract.surfaces[interfacectl-web].layout.maxContentWidth" + } } ], "driftRisks": [ diff --git a/packages/interfacectl-cli/test/fixtures/traceability/contracts/ui.contract.json b/packages/interfacectl-cli/test/fixtures/traceability/contracts/ui.contract.json new file mode 100644 index 0000000..15c2699 --- /dev/null +++ b/packages/interfacectl-cli/test/fixtures/traceability/contracts/ui.contract.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://contracts.surfaces.local/surfaces.web.contract.schema.json", + "contractId": "traceability.fixture", + "version": "0.0.1", + "surfaces": [ + { + "id": "demo-surface", + "displayName": "Demo Surface", + "type": "web", + "requiredSections": ["main.hero"], + "allowedFonts": ["var(--font-demo)", "sans-serif"], + "layout": { + "maxContentWidth": 800 + } + } + ], + "sections": [ + { + "id": "main.hero", + "intent": "primary-intro", + "description": "Demo hero section" + } + ], + "constraints": { + "motion": { + "allowedDurationsMs": [120], + "allowedTimingFunctions": ["linear"] + } + } +} diff --git a/packages/interfacectl-cli/test/fixtures/traceability/interfacectl.config.json b/packages/interfacectl-cli/test/fixtures/traceability/interfacectl.config.json new file mode 100644 index 0000000..bc89fe2 --- /dev/null +++ b/packages/interfacectl-cli/test/fixtures/traceability/interfacectl.config.json @@ -0,0 +1,5 @@ +{ + "surfaceRoots": { + "demo-surface": "src/demo-surface" + } +} diff --git a/packages/interfacectl-cli/test/fixtures/traceability/policy.json b/packages/interfacectl-cli/test/fixtures/traceability/policy.json new file mode 100644 index 0000000..c03abc1 --- /dev/null +++ b/packages/interfacectl-cli/test/fixtures/traceability/policy.json @@ -0,0 +1,26 @@ +{ + "version": "1.0.0", + "fingerprint": "0000000000000000000000000000000000000000000000000000000000000000", + "modes": { + "fail": { + "exitOnAny": true, + "severityThreshold": "error" + }, + "fix": { + "rules": ["layout-maxwidth"], + "dryRun": true + }, + "pr": { + "patchFormat": "json" + } + }, + "autofixRules": [ + { + "id": "layout-maxwidth", + "pattern": "surfaces/*/layout.maxContentWidth", + "autofixable": true, + "description": "Align maxContentWidth to contract", + "safetyLevel": "mechanical" + } + ] +} diff --git a/packages/interfacectl-cli/test/fixtures/traceability/src/demo-surface/app/globals.css b/packages/interfacectl-cli/test/fixtures/traceability/src/demo-surface/app/globals.css new file mode 100644 index 0000000..d6522de --- /dev/null +++ b/packages/interfacectl-cli/test/fixtures/traceability/src/demo-surface/app/globals.css @@ -0,0 +1,23 @@ +:root { + --contract-max-width: 960px; + --contract-motion-duration: 120ms; + --contract-motion-timing: linear; + --color-primary: #000000; + --color-background: #ffffff; +} + +.fixture-body { + --font-demo: "Demo Sans"; + font-family: var(--font-demo), "Demo Sans", sans-serif; + color: var(--color-primary); + background-color: var(--color-background); +} + +.contract-container { + max-width: var(--contract-max-width); + transition: opacity var(--contract-motion-duration) + var(--contract-motion-timing); + animation-duration: var(--contract-motion-duration); + animation-timing-function: var(--contract-motion-timing); +} + diff --git a/packages/interfacectl-cli/test/fixtures/traceability/src/demo-surface/app/layout.tsx b/packages/interfacectl-cli/test/fixtures/traceability/src/demo-surface/app/layout.tsx new file mode 100644 index 0000000..96a880e --- /dev/null +++ b/packages/interfacectl-cli/test/fixtures/traceability/src/demo-surface/app/layout.tsx @@ -0,0 +1,14 @@ +import "./globals.css"; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} + diff --git a/packages/interfacectl-cli/test/fixtures/traceability/src/demo-surface/app/page.tsx b/packages/interfacectl-cli/test/fixtures/traceability/src/demo-surface/app/page.tsx new file mode 100644 index 0000000..2179a25 --- /dev/null +++ b/packages/interfacectl-cli/test/fixtures/traceability/src/demo-surface/app/page.tsx @@ -0,0 +1,11 @@ +export default function FixtureSurface() { + return ( +
+
+

Portable Fixture Surface

+

Validates outside the monorepo.

+
+
+ ); +} + diff --git a/packages/interfacectl-cli/test/traceability.test.mjs b/packages/interfacectl-cli/test/traceability.test.mjs new file mode 100644 index 0000000..701513f --- /dev/null +++ b/packages/interfacectl-cli/test/traceability.test.mjs @@ -0,0 +1,171 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { validateDiffOutput, validateFixSummary } from "@surfaces/interfacectl-validator"; +import { enrichDiffEntry } from "../dist/utils/traceability.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const cliPath = path.resolve(__dirname, "../dist/index.js"); +const traceabilityFixture = path.resolve(__dirname, "fixtures/traceability"); + +function runCLI(args, cwd = process.cwd()) { + return new Promise((resolve, reject) => { + const proc = spawn("node", [cliPath, ...args], { + cwd, + stdio: ["ignore", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + proc.stdout.on("data", (data) => { + stdout += data.toString(); + }); + proc.stderr.on("data", (data) => { + stderr += data.toString(); + }); + proc.on("close", (code) => { + resolve({ code, stdout, stderr }); + }); + proc.on("error", reject); + }); +} + +test("surface-missing entry includes contractRef.surfaceId but omits contractRef.path", () => { + const entry = { + surfaceId: "unknown-surface", + type: "added", + path: "surfaces/unknown-surface", + observedValue: {}, + severity: "error", + rule: "contract.surface-missing", + }; + + const result = enrichDiffEntry(entry); + + assert.ok(result.contractRef); + assert.equal(result.contractRef.surfaceId, "unknown-surface"); + assert.ok(!("path" in result.contractRef) || result.contractRef.path === undefined); +}); + +test("diff --format json emits stableId for every entry", async () => { + const result = await runCLI( + ["diff", "--contract", "contracts/ui.contract.json", "--format", "json"], + traceabilityFixture, + ); + const output = JSON.parse(result.stdout); + const validation = validateDiffOutput(output); + assert.equal( + validation.ok, + true, + `Diff output should pass schema validation: ${validation.errors?.join("; ") ?? "unknown"}`, + ); + assert(output.entries.length > 0, "Fixture should produce at least one diff entry"); + for (const entry of output.entries) { + assert(entry.stableId, `Entry ${entry.path} should have stableId`); + assert.equal(typeof entry.stableId, "string"); + assert(entry.stableId.length >= 1); + } +}); + +test("diff entries include contractRef.path when contract node is unambiguous", async () => { + const result = await runCLI( + ["diff", "--contract", "contracts/ui.contract.json", "--format", "json"], + traceabilityFixture, + ); + const output = JSON.parse(result.stdout); + for (const entry of output.entries) { + assert(entry.contractRef, `Entry ${entry.path} should have contractRef`); + assert(entry.contractRef.path, `Entry ${entry.path} should have contractRef.path`); + assert(entry.contractRef.path.startsWith("/"), "contractRef.path should be JSON pointer style"); + } +}); + +test("diff entries include ruleRef.id when rule is present", async () => { + const result = await runCLI( + ["diff", "--contract", "contracts/ui.contract.json", "--format", "json"], + traceabilityFixture, + ); + const output = JSON.parse(result.stdout); + for (const entry of output.entries) { + assert(entry.ruleRef, `Entry ${entry.path} should have ruleRef`); + assert(entry.ruleRef.id, `Entry ${entry.path} should have ruleRef.id`); + } +}); + +test("stableId is deterministic across runs", async () => { + const run1 = await runCLI( + ["diff", "--contract", "contracts/ui.contract.json", "--format", "json"], + traceabilityFixture, + ); + const run2 = await runCLI( + ["diff", "--contract", "contracts/ui.contract.json", "--format", "json"], + traceabilityFixture, + ); + const out1 = JSON.parse(run1.stdout); + const out2 = JSON.parse(run2.stdout); + assert.equal(out1.entries.length, out2.entries.length); + const stableIds1 = new Map(out1.entries.map((e) => [e.path + "\0" + JSON.stringify(e.observedValue ?? e.contractValue), e.stableId])); + const stableIds2 = new Map(out2.entries.map((e) => [e.path + "\0" + JSON.stringify(e.observedValue ?? e.contractValue), e.stableId])); + for (const [key, id1] of stableIds1) { + const id2 = stableIds2.get(key); + assert(id2, `Second run should have entry for ${key}`); + assert.equal(id1, id2, `stableId should be identical for same logical entry: ${id1} vs ${id2}`); + } +}); + +test("enforce --format json emits stableId for applied and skipped actions", async () => { + const result = await runCLI( + [ + "enforce", + "--mode", + "fix", + "--contract", + "contracts/ui.contract.json", + "--policy", + "policy.json", + "--format", + "json", + "--dry-run", + ], + traceabilityFixture, + ); + const output = JSON.parse(result.stdout); + const validation = validateFixSummary(output); + assert.equal(validation.ok, true, "Fix summary should pass schema validation"); + assert(output.applied.length > 0, "Traceability policy should produce at least one applied fix"); + for (const fix of output.applied) { + assert(fix.stableId, `Applied fix ${fix.path} should have stableId`); + } + for (const fix of output.skipped) { + assert(fix.stableId, `Skipped fix ${fix.path} should have stableId`); + } +}); + +test("enforce output includes contractRef and ruleRef when deterministically derivable", async () => { + const result = await runCLI( + [ + "enforce", + "--mode", + "fix", + "--contract", + "contracts/ui.contract.json", + "--policy", + "policy.json", + "--format", + "json", + "--dry-run", + ], + traceabilityFixture, + ); + const output = JSON.parse(result.stdout); + const allItems = [...output.applied, ...output.skipped]; + for (const item of allItems) { + assert(item.ruleRef, `Item ${item.path} should have ruleRef`); + assert(item.ruleRef.id, `Item ${item.path} should have ruleRef.id`); + if (item.contractRef && Object.keys(item.contractRef).length > 0) { + assert(item.contractRef.path || item.contractRef.surfaceId, "contractRef should have path or surfaceId when present"); + } + } +}); diff --git a/packages/interfacectl-validator/dist/index.d.ts b/packages/interfacectl-validator/dist/index.d.ts index 8049b3e..e460ed6 100644 --- a/packages/interfacectl-validator/dist/index.d.ts +++ b/packages/interfacectl-validator/dist/index.d.ts @@ -8,6 +8,6 @@ export interface ContractStructureValidation { export declare function validateContractStructure(contractData: unknown, schema: object): ContractStructureValidation; export declare function evaluateSurfaceCompliance(contract: InterfaceContract, descriptor: SurfaceDescriptor): SurfaceReport; export declare function evaluateContractCompliance(contract: InterfaceContract, descriptors: SurfaceDescriptor[]): ValidationSummary; -export type { InterfaceContract, ContractSurface, ContractSection, ContractConstraints, SurfaceDescriptor, SurfaceSectionDescriptor, SurfaceFontDescriptor, SurfaceColorDescriptor, SurfaceMotionDescriptor, SurfaceLayoutDescriptor, PageFrameLayoutDescriptor, SurfaceReport, DriftViolation, ValidationSummary, DriftViolationType, DiffOutput, DiffEntry, DiffChangeType, DriftRisk, Severity, SafetyLevel, EnforcementPolicy, EnforcementMode, AutofixRule, FixSummary, FixEntry, FixError, } from "./types.js"; +export type { InterfaceContract, ContractSurface, ContractSection, ContractConstraints, SurfaceDescriptor, SurfaceSectionDescriptor, SurfaceFontDescriptor, SurfaceColorDescriptor, SurfaceMotionDescriptor, SurfaceLayoutDescriptor, PageFrameLayoutDescriptor, SurfaceReport, DriftViolation, ValidationSummary, DriftViolationType, ContractRef, RuleRef, DiffOutput, DiffEntry, DiffChangeType, DriftRisk, Severity, SafetyLevel, EnforcementPolicy, EnforcementMode, AutofixRule, FixSummary, FixEntry, FixError, } from "./types.js"; export { getBundledDiffSchema, getBundledPolicySchema, getBundledFixSummarySchema, validateDiffOutput, validatePolicy, validateFixSummary, type ValidationResult, } from "./schema-validate.js"; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/interfacectl-validator/dist/index.d.ts.map b/packages/interfacectl-validator/dist/index.d.ts.map index b1790f1..660d83c 100644 --- a/packages/interfacectl-validator/dist/index.d.ts.map +++ b/packages/interfacectl-validator/dist/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EAKlB,MAAM,YAAY,CAAC;AAOpB,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED,MAAM,WAAW,2BAA2B;IAC1C,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,OAAO,EACrB,MAAM,EAAE,MAAM,GACb,2BAA2B,CAyB7B;AA4ID,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,iBAAiB,EAC3B,UAAU,EAAE,iBAAiB,GAC5B,aAAa,CAqUf;AAED,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,iBAAiB,EAC3B,WAAW,EAAE,iBAAiB,EAAE,GAC/B,iBAAiB,CA6CnB;AAyCD,YAAY,EACV,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,yBAAyB,EACzB,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,UAAU,EACV,SAAS,EACT,cAAc,EACd,SAAS,EACT,QAAQ,EACR,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,UAAU,EACV,QAAQ,EACR,QAAQ,GACT,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,0BAA0B,EAC1B,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,KAAK,gBAAgB,GACtB,MAAM,sBAAsB,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EAKlB,MAAM,YAAY,CAAC;AAOpB,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED,MAAM,WAAW,2BAA2B;IAC1C,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,OAAO,EACrB,MAAM,EAAE,MAAM,GACb,2BAA2B,CAyB7B;AA4ID,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,iBAAiB,EAC3B,UAAU,EAAE,iBAAiB,GAC5B,aAAa,CAqUf;AAED,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,iBAAiB,EAC3B,WAAW,EAAE,iBAAiB,EAAE,GAC/B,iBAAiB,CA6CnB;AAyCD,YAAY,EACV,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,yBAAyB,EACzB,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,OAAO,EACP,UAAU,EACV,SAAS,EACT,cAAc,EACd,SAAS,EACT,QAAQ,EACR,WAAW,EACX,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,UAAU,EACV,QAAQ,EACR,QAAQ,GACT,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,0BAA0B,EAC1B,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,KAAK,gBAAgB,GACtB,MAAM,sBAAsB,CAAC"} \ No newline at end of file diff --git a/packages/interfacectl-validator/dist/schema/interfacectl.diff.schema.json b/packages/interfacectl-validator/dist/schema/interfacectl.diff.schema.json index a31a2d8..5dc7259 100644 --- a/packages/interfacectl-validator/dist/schema/interfacectl.diff.schema.json +++ b/packages/interfacectl-validator/dist/schema/interfacectl.diff.schema.json @@ -132,6 +132,27 @@ "type": "string", "enum": ["added", "removed", "modified", "renamed"] }, + "ContractRef": { + "type": "object", + "description": "Reference to contract structure when deterministically derivable.", + "additionalProperties": false, + "properties": { + "path": { "type": "string", "description": "JSON pointer style path into the contract." }, + "surfaceId": { "type": "string", "description": "Surface identifier when available." }, + "sectionId": { "type": "string", "description": "Section identifier when available." }, + "constraintId": { "type": "string", "description": "Constraint category when applicable (e.g. motion)." } + } + }, + "RuleRef": { + "type": "object", + "description": "Reference to the rule that produced this entry.", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": { "type": "string", "minLength": 1, "description": "Stable rule identifier." }, + "version": { "type": "string", "description": "Optional rule version when catalog exists." } + } + }, "DiffEntry": { "type": "object", "additionalProperties": false, @@ -170,7 +191,14 @@ "toPath": { "type": "string", "minLength": 1 }, "confidence": { "type": "number", "minimum": 0, "maximum": 1 } } - } + }, + "stableId": { + "type": "string", + "description": "Deterministic id for correlation across runs.", + "minLength": 1 + }, + "contractRef": { "$ref": "#/$defs/ContractRef" }, + "ruleRef": { "$ref": "#/$defs/RuleRef" } }, "allOf": [ { diff --git a/packages/interfacectl-validator/dist/schema/interfacectl.fix-summary.schema.json b/packages/interfacectl-validator/dist/schema/interfacectl.fix-summary.schema.json index ccb7161..5676bcd 100644 --- a/packages/interfacectl-validator/dist/schema/interfacectl.fix-summary.schema.json +++ b/packages/interfacectl-validator/dist/schema/interfacectl.fix-summary.schema.json @@ -34,6 +34,26 @@ } }, "$defs": { + "ContractRef": { + "type": "object", + "description": "Reference to contract structure when deterministically derivable.", + "additionalProperties": false, + "properties": { + "path": { "type": "string" }, + "surfaceId": { "type": "string" }, + "sectionId": { "type": "string" }, + "constraintId": { "type": "string" } + } + }, + "RuleRef": { + "type": "object", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "version": { "type": "string" } + } + }, "FixEntry": { "type": "object", "additionalProperties": false, @@ -51,7 +71,10 @@ "lineDelta": { "type": "integer", "description": "Optional net line change count for patch summaries." - } + }, + "stableId": { "type": "string", "minLength": 1 }, + "contractRef": { "$ref": "#/$defs/ContractRef" }, + "ruleRef": { "$ref": "#/$defs/RuleRef" } } }, "FixError": { @@ -61,7 +84,10 @@ "properties": { "ruleId": { "type": "string", "minLength": 1 }, "path": { "type": "string", "minLength": 1 }, - "message": { "type": "string", "minLength": 1 } + "message": { "type": "string", "minLength": 1 }, + "stableId": { "type": "string", "minLength": 1 }, + "contractRef": { "$ref": "#/$defs/ContractRef" }, + "ruleRef": { "$ref": "#/$defs/RuleRef" } } } } diff --git a/packages/interfacectl-validator/dist/types.d.ts b/packages/interfacectl-validator/dist/types.d.ts index 07ad9ee..5ee66dc 100644 --- a/packages/interfacectl-validator/dist/types.d.ts +++ b/packages/interfacectl-validator/dist/types.d.ts @@ -131,6 +131,16 @@ export type DiffChangeType = "added" | "removed" | "modified" | "renamed"; export type Severity = "error" | "warning" | "info"; export type SafetyLevel = "safe" | "mechanical" | "semantic"; export type EnforcementMode = "fail" | "fix" | "pr"; +export interface ContractRef { + path?: string; + surfaceId?: string; + sectionId?: string; + constraintId?: string; +} +export interface RuleRef { + id: string; + version?: string; +} export interface DiffEntry { surfaceId?: string; type: DiffChangeType; @@ -145,6 +155,12 @@ export interface DiffEntry { toPath: string; confidence: number; }; + /** Deterministic id for correlation across runs (Phase 2 traceability) */ + stableId?: string; + /** Reference to contract structure when deterministically derivable */ + contractRef?: ContractRef; + /** Reference to the rule that produced this entry */ + ruleRef?: RuleRef; } export interface DriftRisk { category: "diff-noise" | "semantic-ambiguity" | "enforcement-overreach" | "policy-drift" | "contract-evolution" | "observed-instability" | "rename-inflation" | "output-schema-drift" | "severity-inflation" | "local-ci-mismatch"; @@ -239,11 +255,23 @@ export interface FixEntry { confidence: number; file?: string; lineDelta?: number; + /** Deterministic id for correlation across runs (Phase 2 traceability) */ + stableId?: string; + /** Reference to contract structure when deterministically derivable */ + contractRef?: ContractRef; + /** Reference to the rule that produced this entry */ + ruleRef?: RuleRef; } export interface FixError { ruleId: string; path: string; message: string; + /** Deterministic id for correlation across runs (Phase 2 traceability) */ + stableId?: string; + /** Reference to contract structure when deterministically derivable */ + contractRef?: ContractRef; + /** Reference to the rule that produced this entry */ + ruleRef?: RuleRef; } export interface FixSummary { schemaVersion: string; diff --git a/packages/interfacectl-validator/dist/types.d.ts.map b/packages/interfacectl-validator/dist/types.d.ts.map index f7a0502..30e3346 100644 --- a/packages/interfacectl-validator/dist/types.d.ts.map +++ b/packages/interfacectl-validator/dist/types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/interfacectl-validator/src/index.ts b/packages/interfacectl-validator/src/index.ts index 0238eee..160436c 100644 --- a/packages/interfacectl-validator/src/index.ts +++ b/packages/interfacectl-validator/src/index.ts @@ -629,6 +629,8 @@ export type { DriftViolation, ValidationSummary, DriftViolationType, + ContractRef, + RuleRef, DiffOutput, DiffEntry, DiffChangeType, diff --git a/packages/interfacectl-validator/src/schema/interfacectl.diff.schema.json b/packages/interfacectl-validator/src/schema/interfacectl.diff.schema.json index a31a2d8..5dc7259 100644 --- a/packages/interfacectl-validator/src/schema/interfacectl.diff.schema.json +++ b/packages/interfacectl-validator/src/schema/interfacectl.diff.schema.json @@ -132,6 +132,27 @@ "type": "string", "enum": ["added", "removed", "modified", "renamed"] }, + "ContractRef": { + "type": "object", + "description": "Reference to contract structure when deterministically derivable.", + "additionalProperties": false, + "properties": { + "path": { "type": "string", "description": "JSON pointer style path into the contract." }, + "surfaceId": { "type": "string", "description": "Surface identifier when available." }, + "sectionId": { "type": "string", "description": "Section identifier when available." }, + "constraintId": { "type": "string", "description": "Constraint category when applicable (e.g. motion)." } + } + }, + "RuleRef": { + "type": "object", + "description": "Reference to the rule that produced this entry.", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": { "type": "string", "minLength": 1, "description": "Stable rule identifier." }, + "version": { "type": "string", "description": "Optional rule version when catalog exists." } + } + }, "DiffEntry": { "type": "object", "additionalProperties": false, @@ -170,7 +191,14 @@ "toPath": { "type": "string", "minLength": 1 }, "confidence": { "type": "number", "minimum": 0, "maximum": 1 } } - } + }, + "stableId": { + "type": "string", + "description": "Deterministic id for correlation across runs.", + "minLength": 1 + }, + "contractRef": { "$ref": "#/$defs/ContractRef" }, + "ruleRef": { "$ref": "#/$defs/RuleRef" } }, "allOf": [ { diff --git a/packages/interfacectl-validator/src/schema/interfacectl.fix-summary.schema.json b/packages/interfacectl-validator/src/schema/interfacectl.fix-summary.schema.json index ccb7161..5676bcd 100644 --- a/packages/interfacectl-validator/src/schema/interfacectl.fix-summary.schema.json +++ b/packages/interfacectl-validator/src/schema/interfacectl.fix-summary.schema.json @@ -34,6 +34,26 @@ } }, "$defs": { + "ContractRef": { + "type": "object", + "description": "Reference to contract structure when deterministically derivable.", + "additionalProperties": false, + "properties": { + "path": { "type": "string" }, + "surfaceId": { "type": "string" }, + "sectionId": { "type": "string" }, + "constraintId": { "type": "string" } + } + }, + "RuleRef": { + "type": "object", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "version": { "type": "string" } + } + }, "FixEntry": { "type": "object", "additionalProperties": false, @@ -51,7 +71,10 @@ "lineDelta": { "type": "integer", "description": "Optional net line change count for patch summaries." - } + }, + "stableId": { "type": "string", "minLength": 1 }, + "contractRef": { "$ref": "#/$defs/ContractRef" }, + "ruleRef": { "$ref": "#/$defs/RuleRef" } } }, "FixError": { @@ -61,7 +84,10 @@ "properties": { "ruleId": { "type": "string", "minLength": 1 }, "path": { "type": "string", "minLength": 1 }, - "message": { "type": "string", "minLength": 1 } + "message": { "type": "string", "minLength": 1 }, + "stableId": { "type": "string", "minLength": 1 }, + "contractRef": { "$ref": "#/$defs/ContractRef" }, + "ruleRef": { "$ref": "#/$defs/RuleRef" } } } } diff --git a/packages/interfacectl-validator/src/types.ts b/packages/interfacectl-validator/src/types.ts index 339e7f3..8379dc6 100644 --- a/packages/interfacectl-validator/src/types.ts +++ b/packages/interfacectl-validator/src/types.ts @@ -177,6 +177,18 @@ export type Severity = "error" | "warning" | "info"; export type SafetyLevel = "safe" | "mechanical" | "semantic"; export type EnforcementMode = "fail" | "fix" | "pr"; +export interface ContractRef { + path?: string; + surfaceId?: string; + sectionId?: string; + constraintId?: string; +} + +export interface RuleRef { + id: string; + version?: string; +} + export interface DiffEntry { surfaceId?: string; type: DiffChangeType; @@ -191,6 +203,12 @@ export interface DiffEntry { toPath: string; confidence: number; // 0-1 }; // Only present when type=renamed + /** Deterministic id for correlation across runs (Phase 2 traceability) */ + stableId?: string; + /** Reference to contract structure when deterministically derivable */ + contractRef?: ContractRef; + /** Reference to the rule that produced this entry */ + ruleRef?: RuleRef; } export interface DriftRisk { @@ -264,12 +282,24 @@ export interface FixEntry { confidence: number; // 0-1 file?: string; // Relative path only lineDelta?: number; + /** Deterministic id for correlation across runs (Phase 2 traceability) */ + stableId?: string; + /** Reference to contract structure when deterministically derivable */ + contractRef?: ContractRef; + /** Reference to the rule that produced this entry */ + ruleRef?: RuleRef; } export interface FixError { ruleId: string; path: string; message: string; + /** Deterministic id for correlation across runs (Phase 2 traceability) */ + stableId?: string; + /** Reference to contract structure when deterministically derivable */ + contractRef?: ContractRef; + /** Reference to the rule that produced this entry */ + ruleRef?: RuleRef; } export interface FixSummary {