Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e107145
fix(iae): fail closed for unavailable evidence
BeforeLights Aug 2, 2026
e52bd2a
feat(iae): expose safe evidence resolution
BeforeLights Aug 2, 2026
7927cf2
fix(iae): make evidence persistence idempotent
BeforeLights Aug 2, 2026
081147b
fix(sa): validate finding geometry
BeforeLights Aug 2, 2026
2563aab
feat(iae): persist artifact scan state
BeforeLights Aug 2, 2026
d4cda4d
feat(iae): expose retention request reads
BeforeLights Aug 2, 2026
70a4596
fix(dsm): enforce deterministic quality state
BeforeLights Aug 2, 2026
9312775
fix(iae): reject invalid lineage references
BeforeLights Aug 2, 2026
6431c9a
feat(iae): enforce one lineage per derived version
BeforeLights Aug 2, 2026
556dd73
test(sa): ignore correlation id in content assertion
BeforeLights Aug 2, 2026
968168e
feat(sa): detect deterministic formula gaps
BeforeLights Aug 2, 2026
d87cdac
fix(iae): validate evidence geometry before lookup
BeforeLights Aug 2, 2026
27789f7
fix(sa): preserve absolute formula references
BeforeLights Aug 2, 2026
4d3f40d
fix(api): expose audit and entitlement composition options
BeforeLights Aug 2, 2026
868c573
fix(iam): wrap direct bootstrap saves in transactions
BeforeLights Aug 2, 2026
237ba56
fix(iam): guard membership updates by revision
BeforeLights Aug 2, 2026
bc069e2
fix(iae): guard placement updates by revision
BeforeLights Aug 2, 2026
68e69df
fix(iae): use unique lookup for derived lineage
BeforeLights Aug 2, 2026
e2ccd90
fix(iae): bind retention requests to actor context
BeforeLights Aug 2, 2026
221c3c6
fix(iae): guard retention transitions by revision
BeforeLights Aug 2, 2026
536a2d1
fix(iae): guard artifact status transitions by scan state
BeforeLights Aug 2, 2026
91b63ba
test(iae): cover retention HTTP actor binding
BeforeLights Aug 2, 2026
9bbcb53
fix(iam): reject changed stale MFA revisions
BeforeLights Aug 2, 2026
068286b
fix(iam): guard device transitions by revision
BeforeLights Aug 2, 2026
216f4a1
fix(bua): guard reservation settlement by revision
BeforeLights Aug 2, 2026
735c1f3
fix(dso): guard capability replacement by revision
BeforeLights Aug 2, 2026
98f7f75
fix(dso): guard sync transitions by revision
BeforeLights Aug 2, 2026
49e7426
fix(dso): guard grant revocation by revision
BeforeLights Aug 2, 2026
ebf76c5
fix(dsm): validate immutable dataset revisions
BeforeLights Aug 2, 2026
9d3f82e
fix(dsm): validate immutable mapping revisions
BeforeLights Aug 2, 2026
21be92e
fix(style): format artifact domain tests
BeforeLights Aug 2, 2026
1336ddc
fix(tooling): ignore worktrees in lint
BeforeLights Aug 2, 2026
48ede83
fix(engine): satisfy ruff pairwise checks
BeforeLights Aug 2, 2026
c9cac6b
fix(api): satisfy strict correlation redaction typing
BeforeLights Aug 2, 2026
8f50039
fix(engine): format spreadsheet auditor fixtures
BeforeLights Aug 2, 2026
12d9271
Merge pull request #27 from DatabreezeService/feat/iae-evidence-gover…
BeforeLights Aug 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default tseslint.config(
'**/out/**',
'packages/contracts/generated/**',
'tools/repo-cli/test/fixtures/**',
'.worktrees/**',
],
},
eslint.configs.recommended,
Expand Down
8 changes: 8 additions & 0 deletions packages/domain/src/artifact-governance/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,12 @@ export function createArtifactLineageV1(input: {
return rejected('INVALID_IDENTIFIER');
if (new Set(sourceArtifactVersionIds).size !== sourceArtifactVersionIds.length)
return rejected('DUPLICATE_IDENTIFIER');
if (sourceArtifactVersionIds.includes(derivedArtifactVersionId))
return rejected('DUPLICATE_IDENTIFIER');
if (input.sourceTenantScopes !== undefined) {
if (!Array.isArray(input.sourceTenantScopes)) return rejected('INVALID_SCOPE');
if (input.sourceTenantScopes.length !== sourceArtifactVersionIds.length)
return rejected('INVALID_SCOPE');
for (const candidate of input.sourceTenantScopes) {
const sourceScope = scope(candidate);
if (!sourceScope || !tenantScopesEqualV1(sourceScope, tenantScope))
Expand All @@ -129,6 +133,7 @@ export function createArtifactLineageV1(input: {
}
if (!Array.isArray(input.coordinateLineage)) return rejected('INVALID_LINEAGE');
const coordinateLineage: CoordinateLineageV1[] = [];
const coordinatePairs = new Set<string>();
for (const candidate of input.coordinateLineage) {
if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate))
return rejected('INVALID_LINEAGE');
Expand All @@ -139,6 +144,9 @@ export function createArtifactLineageV1(input: {
if (!sourceEvidenceId || !derivedEvidenceId) return rejected('INVALID_IDENTIFIER');
if (!['COPIED', 'NORMALIZED', 'AGGREGATED', 'REDACTED'].includes(transform as string))
return rejected('INVALID_TRANSFORM');
const pair = `${sourceEvidenceId}:${derivedEvidenceId}`;
if (coordinatePairs.has(pair)) return rejected('DUPLICATE_IDENTIFIER');
coordinatePairs.add(pair);
coordinateLineage.push(
Object.freeze({
sourceEvidenceId,
Expand Down
4 changes: 3 additions & 1 deletion packages/domain/src/artifact-intake/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
type TenantScopeV1,
} from '../tenant-scope/v1.js';
import type { ArtifactVersionV1 } from '../artifact/v1.js';
import type { ArtifactScanStateV1 } from '../artifact/v1.js';

export type { ArtifactScanStateV1 } from '../artifact/v1.js';

/** IAE-001, IAE-009, IAE-010, IAE-013: intake admission is explicit and idempotent. */
export const ARTIFACT_INTAKE_SCHEMA_VERSION_V1 = 1 as const;
Expand All @@ -20,7 +23,6 @@ export type InboxItemStateV1 =
| 'RESOLVED'
| 'QUARANTINED'
| 'ARCHIVED';
export type ArtifactScanStateV1 = 'PENDING' | 'CLEAN' | 'MALICIOUS' | 'FAILED';
export type InboxPriorityV1 = 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT';

export interface InboxItemV1 {
Expand Down
47 changes: 47 additions & 0 deletions packages/domain/src/artifact/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const ARTIFACT_SCHEMA_VERSION_V1 = 1 as const;
export type ArtifactDataModeV1 = 'Local' | 'Hybrid' | 'Cloud';
export type ArtifactSourceKindV1 = 'FILE' | 'FOLDER' | 'CAPTURE' | 'GENERATED';
export type ArtifactVersionStatusV1 = 'QUARANTINED' | 'ACTIVE' | 'DELETED';
export type ArtifactScanStateV1 = 'PENDING' | 'CLEAN' | 'MALICIOUS' | 'FAILED';
export type ArtifactPlacementKindV1 = 'LOCAL' | 'CLOUD';
export type EvidenceSourceStateV1 = 'AVAILABLE' | 'SOURCE_OFFLINE' | 'DELETED';

Expand Down Expand Up @@ -42,6 +43,7 @@ export interface ArtifactVersionV1 {
readonly displayName: string;
readonly createdAt: StrictUtcTimestampV1;
readonly status: ArtifactVersionStatusV1;
readonly scanState: ArtifactScanStateV1;
}

export interface ContentPlacementV1 {
Expand Down Expand Up @@ -83,6 +85,7 @@ export type ArtifactErrorCodeV1 =
| 'INVALID_MEDIA_TYPE'
| 'INVALID_NAME'
| 'INVALID_STATUS'
| 'INVALID_SCAN_STATE'
| 'INVALID_REVISION'
| 'REVISION_CONFLICT'
| 'INVALID_REFERENCE'
Expand Down Expand Up @@ -172,6 +175,10 @@ function isStatus(input: unknown): input is ArtifactVersionStatusV1 {
return input === 'QUARANTINED' || input === 'ACTIVE' || input === 'DELETED';
}

function isScanState(input: unknown): input is ArtifactScanStateV1 {
return input === 'PENDING' || input === 'CLEAN' || input === 'MALICIOUS' || input === 'FAILED';
}

export function createArtifactVersionV1(input: {
readonly artifactId: unknown;
readonly versionId: unknown;
Expand All @@ -184,6 +191,7 @@ export function createArtifactVersionV1(input: {
readonly displayName: unknown;
readonly createdAt: unknown;
readonly status?: unknown;
readonly scanState?: unknown;
}): ArtifactResultV1<ArtifactVersionV1> {
const artifactId = stableId(input.artifactId);
const versionId = stableId(input.versionId);
Expand All @@ -193,6 +201,7 @@ export function createArtifactVersionV1(input: {
const displayNameValue = displayName(input.displayName);
const createdAt = timestamp(input.createdAt);
const status = input.status ?? 'ACTIVE';
const scanState = input.scanState ?? 'PENDING';
if (!artifactId || !versionId) return rejected('INVALID_IDENTIFIER');
if (!tenantScope) return rejected('INVALID_SCOPE');
if (!isSourceKind(input.sourceKind)) return rejected('INVALID_KIND');
Expand All @@ -208,6 +217,7 @@ export function createArtifactVersionV1(input: {
if (!displayNameValue) return rejected('INVALID_NAME');
if (!createdAt) return rejected('INVALID_TIMESTAMP');
if (!isStatus(status)) return rejected('INVALID_STATUS');
if (!isScanState(scanState)) return rejected('INVALID_SCAN_STATE');
return accepted(
Object.freeze({
schemaVersion: ARTIFACT_SCHEMA_VERSION_V1,
Expand All @@ -222,6 +232,7 @@ export function createArtifactVersionV1(input: {
displayName: displayNameValue,
createdAt,
status,
scanState,
}),
);
}
Expand Down Expand Up @@ -330,12 +341,48 @@ function spreadsheetColumnNumber(value: string): number {
return result;
}

function nonNegativeCount(input: unknown): input is number {
return typeof input === 'number' && Number.isSafeInteger(input) && input >= 0;
}

function isEvidenceGeometry(input: unknown): input is EvidenceGeometryV1 {
if (typeof input !== 'object' || input === null || Array.isArray(input)) return false;
const record = input as Record<string, unknown>;
if (record['kind'] === 'SPREADSHEET') {
if (!Array.isArray(record['sheets']) || record['sheets'].length > 512) return false;
const names = new Set<string>();
return record['sheets'].every((candidate) => {
if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate))
return false;
const sheet = candidate as Record<string, unknown>;
const name = boundedText(sheet['name'], 255);
if (
!name ||
names.has(name) ||
!nonNegativeCount(sheet['maxRow']) ||
!nonNegativeCount(sheet['maxColumn']) ||
sheet['maxRow'] > 1_000_000 ||
sheet['maxColumn'] > 16_384
)
return false;
names.add(name);
return true;
});
}
if (record['kind'] === 'PAGED')
return nonNegativeCount(record['maxPage']) && record['maxPage'] <= 10_000_000;
if (record['kind'] === 'TABULAR')
return nonNegativeCount(record['maxRow']) && record['maxRow'] <= 1_000_000_000;
return false;
}

/** IAE-006: evidence coordinates are checked against the exact source geometry. */
export function validateEvidenceCoordinateV1(
coordinate: EvidenceCoordinateV1,
geometry?: EvidenceGeometryV1,
): ArtifactResultV1<true> {
if (!geometry) return accepted(true);
if (!isEvidenceGeometry(geometry)) return rejected('INVALID_COORDINATE');
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (coordinate.kind === 'CELL') {
if (geometry.kind !== 'SPREADSHEET') return rejected('COORDINATE_OUT_OF_BOUNDS');
const sheet = geometry.sheets.find((candidate) => candidate.name === coordinate.sheet);
Expand Down
5 changes: 5 additions & 0 deletions packages/domain/src/dataset-quality/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,11 @@ export function createDatasetQualityResultV1(input: {
const typedFindings = findings as DatasetQualityFindingV1[];
if (new Set(typedFindings.map((candidate) => candidate.findingId)).size !== typedFindings.length)
return rejected('DUPLICATE_FINDING');
if (
input.qualityState !== 'INCOMPLETE' &&
qualityStateFromFindingsV1(typedFindings) !== input.qualityState
)
return rejected('INVALID_QUALITY_STATE');
return accepted(
Object.freeze({
schemaVersion: DATASET_QUALITY_SCHEMA_VERSION_V1,
Expand Down
20 changes: 17 additions & 3 deletions packages/domain/src/spreadsheet-audit/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ function count(input: unknown): number | undefined {
return typeof input === 'number' && Number.isSafeInteger(input) && input >= 0 ? input : undefined;
}

function columnNumber(value: string): number {
let result = 0;
for (const character of value) result = result * 26 + character.charCodeAt(0) - 64;
return result;
}

function sheet(input: unknown): SpreadsheetAuditSheetV1 | undefined {
if (typeof input !== 'object' || input === null || Array.isArray(input)) return undefined;
const record = input as Record<string, unknown>;
Expand Down Expand Up @@ -181,9 +187,17 @@ export function createSpreadsheetAuditResultV1(input: {
const validFindings = findings as SpreadsheetAuditFindingV1[];
if (new Set(validFindings.map((candidate) => candidate.findingId)).size !== validFindings.length)
return rejected('DUPLICATE_IDENTIFIER');
const sheetIds = new Set(validSheets.map((candidate) => candidate.sheetId));
if (validFindings.some((candidate) => !sheetIds.has(candidate.sheetId)))
return rejected('INVALID_IDENTIFIER');
const sheetsById = new Map(validSheets.map((candidate) => [candidate.sheetId, candidate]));
for (const candidate of validFindings) {
const targetSheet = sheetsById.get(candidate.sheetId);
if (!targetSheet) return rejected('INVALID_IDENTIFIER');
const address = /^([A-Z]{1,3})([1-9][0-9]*)$/u.exec(candidate.address);
if (!address) return rejected('INVALID_COORDINATE');
const column = columnNumber(address[1] ?? '');
const row = Number(address[2]);
if (column > targetSheet.maxColumn || row > targetSheet.maxRow)
return rejected('INVALID_COORDINATE');
}
if (!Array.isArray(input.blockedReasons) || input.blockedReasons.length > 3)
return rejected('INVALID_BLOCKED_REASON');
const validBlockedReasons: SpreadsheetAuditBlockedReasonV1[] = [];
Expand Down
35 changes: 35 additions & 0 deletions packages/domain/test/artifact-governance-v1.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,41 @@ void test('[IAE-003, IAE-007, IAE-012] lineage pins source versions and typed tr
if (result.accepted) assert.equal(result.value.coordinateLineage[0]?.transform, 'NORMALIZED');
});

void test('[IAE-007] lineage rejects self-references, scope cardinality drift, and duplicate coordinate mappings', () => {
const base = {
lineageId: '00000000-0000-4000-8000-000000000015',
derivedArtifactVersionId: '00000000-0000-4000-8000-000000000016',
tenantScope: scope,
sourceArtifactVersionIds: ['00000000-0000-4000-8000-000000000017'],
processorVersion: 'normalizer@1',
coordinateLineage: [
{
sourceEvidenceId: '00000000-0000-4000-8000-000000000018',
derivedEvidenceId: '00000000-0000-4000-8000-000000000019',
transform: 'COPIED',
},
],
};
assert.deepEqual(
createArtifactLineageV1({
...base,
sourceArtifactVersionIds: [base.derivedArtifactVersionId],
}),
{ accepted: false, code: 'DUPLICATE_IDENTIFIER' },
);
assert.deepEqual(createArtifactLineageV1({ ...base, sourceTenantScopes: [] }), {
accepted: false,
code: 'INVALID_SCOPE',
});
assert.deepEqual(
createArtifactLineageV1({
...base,
coordinateLineage: [...base.coordinateLineage, ...base.coordinateLineage],
}),
{ accepted: false, code: 'DUPLICATE_IDENTIFIER' },
);
});

void test('[IAE-008] derived data mode cannot be wider than its least-permissive source', () => {
const source = createArtifactVersionV1({
artifactId: '00000000-0000-4000-8000-000000000020',
Expand Down
23 changes: 23 additions & 0 deletions packages/domain/test/artifact-v1.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,21 @@ void test('[IAE-001, IAE-003] artifact versions normalize and freeze immutable m
assert.equal(result.accepted, true);
if (!result.accepted) return;
assert.equal(result.value.contentSha256, 'a'.repeat(64));
assert.equal(result.value.scanState, 'PENDING');
assert.equal(Object.isFrozen(result.value), true);
});

void test('[IAE-009, IAE-010] artifact scan state is bounded and immutable metadata includes it', () => {
const result = createArtifactVersionV1({ ...base, scanState: 'MALICIOUS' });
assert.equal(result.accepted, true);
if (!result.accepted) return;
assert.equal(result.value.scanState, 'MALICIOUS');
assert.deepEqual(createArtifactVersionV1({ ...base, scanState: 'UNKNOWN' }), {
accepted: false,
code: 'INVALID_SCAN_STATE',
});
});

void test('[IAE-002, DSO-003] Local artifacts accept only opaque local placements', () => {
const artifact = createArtifactVersionV1(base);
assert.equal(artifact.accepted, true);
Expand Down Expand Up @@ -113,4 +125,15 @@ void test('[IAE-006] evidence coordinates are validated against exact source geo
validateEvidenceCoordinateV1({ kind: 'PAGE', page: 4 }, { kind: 'PAGED', maxPage: 3 }),
{ accepted: false, code: 'COORDINATE_OUT_OF_BOUNDS' },
);
assert.deepEqual(
validateEvidenceCoordinateV1(
{ kind: 'CELL', sheet: 'Sheet1', address: 'B4' },
{ kind: 'SPREADSHEET', sheets: null },
),
{ accepted: false, code: 'INVALID_COORDINATE' },
);
assert.deepEqual(
validateEvidenceCoordinateV1({ kind: 'PAGE', page: 1 }, { kind: 'PAGED', maxPage: '3' }),
{ accepted: false, code: 'INVALID_COORDINATE' },
);
});
4 changes: 4 additions & 0 deletions packages/domain/test/dataset-quality-v1.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ void test('[DSM-020] quality state is deterministic from finding severity and co
'BLOCKED',
);
assert.equal(qualityStateFromFindingsV1([], true), 'INCOMPLETE');
assert.deepEqual(result({ qualityState: 'PASS' }), {
accepted: false,
code: 'INVALID_QUALITY_STATE',
});
});

void test('[DSM-013] quality result validation rejects malformed hashes, counts, and duplicate findings', () => {
Expand Down
17 changes: 17 additions & 0 deletions packages/domain/test/spreadsheet-audit-v1.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,20 @@ void test('[SA-005] findings cannot reference an unknown sheet or duplicate IDs'
{ accepted: false, code: 'DUPLICATE_IDENTIFIER' },
);
});

void test('[SA-006] findings must stay inside the exact sheet geometry', () => {
assert.deepEqual(
createSpreadsheetAuditResultV1({
...base,
findings: [{ ...base.findings[0], address: 'E1' }],
}),
{ accepted: false, code: 'INVALID_COORDINATE' },
);
assert.deepEqual(
createSpreadsheetAuditResultV1({
...base,
findings: [{ ...base.findings[0], address: 'A11' }],
}),
{ accepted: false, code: 'INVALID_COORDINATE' },
);
});
Loading
Loading