Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
8022db1
feat(iae): expose exact artifact version reads
BeforeLights Aug 2, 2026
b06482a
feat(iae): expose artifact lineage reads
BeforeLights Aug 2, 2026
56ed43a
feat(iae): version content placement availability
BeforeLights Aug 2, 2026
474843f
feat(iae): model explicit artifact deletion authorization
BeforeLights Aug 2, 2026
bf197f4
feat(iae): add governed retention request service
BeforeLights Aug 2, 2026
c678bed
feat(iae): define verifiable artifact export manifests
BeforeLights Aug 2, 2026
33e2fe6
feat(iae): persist artifact export manifests
BeforeLights Aug 2, 2026
98bcb59
feat(dsm): expose governed dataset publication APIs
BeforeLights Aug 2, 2026
aef376c
feat(dsm): expose mapping publication endpoint
BeforeLights Aug 2, 2026
2269787
feat(dsm): expose rule-set publication endpoint
BeforeLights Aug 2, 2026
66d2a11
feat(dsm): expose reference entity version history
BeforeLights Aug 2, 2026
f23dc51
feat(dsm): persist immutable dataset result manifests
BeforeLights Aug 2, 2026
b36f66d
feat(iae): add retention and export persistence schema
BeforeLights Aug 2, 2026
538cff4
fix(iae): make Prisma placement registration idempotent
BeforeLights Aug 2, 2026
2ce24da
feat(dsm): add Prisma dataset version adapter
BeforeLights Aug 2, 2026
9808e8f
feat(iae): add Prisma retention repository
BeforeLights Aug 2, 2026
8d1baac
feat(iae): add Prisma export repository
BeforeLights Aug 2, 2026
72131d1
feat(iae): add Prisma lineage repository
BeforeLights Aug 2, 2026
9bbc79e
fix(iae): persist validated inbox transitions
BeforeLights Aug 2, 2026
1ca8cc2
feat(iae): add resumable upload domain
BeforeLights Aug 2, 2026
67713f2
feat(iae): coordinate resumable upload sessions
BeforeLights Aug 2, 2026
3fbb581
feat(iae): persist resumable upload sessions
BeforeLights Aug 2, 2026
270ceb1
feat(iae): add Prisma upload session adapter
BeforeLights Aug 2, 2026
1970f55
feat(iae): expose upload session control plane
BeforeLights Aug 2, 2026
a077152
feat(iae): add Prisma evidence grant adapter
BeforeLights Aug 2, 2026
0bb9391
fix(dsm): enforce dataset result input validation
BeforeLights Aug 2, 2026
11bb7b7
feat(dsm): list dataset result manifests
BeforeLights Aug 2, 2026
7da9117
feat(iae): persist artifact admission status
BeforeLights Aug 2, 2026
ad9857b
feat(iae): coordinate artifact admission
BeforeLights Aug 2, 2026
f54d096
feat(iae): expose artifact admission endpoint
BeforeLights Aug 2, 2026
a860c91
fix(iae): normalize retention adapter formatting
BeforeLights Aug 2, 2026
f496647
fix(test): satisfy strict API lint gates
BeforeLights Aug 2, 2026
356a4ff
chore(api): refresh generated OpenAPI and migration inventory
BeforeLights Aug 2, 2026
8e430ba
feat(dsm): add immutable dataset quality result contract
BeforeLights Aug 2, 2026
b4e1873
feat(dsm): coordinate tenant-scoped quality results
BeforeLights Aug 2, 2026
53acdea
feat(dsm): persist quality results with Prisma
BeforeLights Aug 2, 2026
9c12117
feat(dsm): expose dataset quality result API
BeforeLights Aug 2, 2026
e87d729
feat(engine): evaluate value-free dataset quality rules
BeforeLights Aug 2, 2026
6b3c8cd
feat(iae): add revisioned inbox metadata policy
BeforeLights Aug 2, 2026
76e7053
feat(iae): coordinate inbox metadata updates
BeforeLights Aug 2, 2026
e6207c2
feat(iae): persist inbox metadata with Prisma
BeforeLights Aug 2, 2026
0aba003
feat(iae): expose revisioned inbox metadata patch
BeforeLights Aug 2, 2026
ecd89c9
fix(test): remove unsafe quality response assertion
BeforeLights Aug 2, 2026
511c483
feat(dsm): add reproducible profile disclosure contract
BeforeLights Aug 2, 2026
612c1fe
feat(dsm): coordinate scoped profile disclosures
BeforeLights Aug 2, 2026
ef43306
feat(dsm): expose profile disclosure API
BeforeLights Aug 2, 2026
384213b
feat(dsm): persist profile disclosures with Prisma
BeforeLights Aug 2, 2026
e415fa7
feat(dsm): add typed safe finding subjects
BeforeLights Aug 2, 2026
261c8af
feat(dsm): expose typed safe finding values
BeforeLights Aug 2, 2026
083e076
feat(dsm): add stable profile pagination
BeforeLights Aug 2, 2026
ebadd9d
feat(dsm): expose cursor-paginated profiles
BeforeLights Aug 2, 2026
ddb7fd2
feat(iae): add opaque upload storage boundary
BeforeLights Aug 2, 2026
56b61b1
feat(iae): verify uploads through storage boundary
BeforeLights Aug 2, 2026
79ebbfd
feat(iae): expose opaque upload transfer grants
BeforeLights Aug 2, 2026
83982e8
feat(iae): add secret-free protected document unlock contract
BeforeLights Aug 2, 2026
b2beaaf
feat(iae): coordinate protected document unlock attempts
BeforeLights Aug 2, 2026
25863a0
feat(iae): expose protected document unlock workflow
BeforeLights Aug 2, 2026
06272d4
feat(iae): persist protected document unlock state
BeforeLights Aug 2, 2026
f978b19
feat(dsm): add governed dataset export manifest contract
BeforeLights Aug 2, 2026
66b6e49
feat(dsm): coordinate governed export manifests
BeforeLights Aug 2, 2026
f3d6356
feat(dsm): expose governed dataset export manifests
BeforeLights Aug 2, 2026
e396283
feat(dsm): persist governed export manifests
BeforeLights Aug 2, 2026
27f8a56
fix(iae): clean expired upload storage state
BeforeLights Aug 2, 2026
135cac8
feat(sa): add safe deterministic spreadsheet auditor core
BeforeLights Aug 2, 2026
7a03e21
feat(sa): add value-free spreadsheet audit result contract
BeforeLights Aug 2, 2026
032ba16
feat(sa): coordinate immutable spreadsheet audit results
BeforeLights Aug 2, 2026
3a88d39
feat(sa): expose spreadsheet audit API boundary
BeforeLights Aug 2, 2026
534b518
feat(sa): persist spreadsheet audit results
BeforeLights Aug 2, 2026
bdd03af
feat(sa): bridge engine audits to value-free manifests
BeforeLights Aug 2, 2026
45c4ed0
docs(sa): record vertical slice evidence and limits
BeforeLights Aug 2, 2026
203cc7a
fix(foundation): keep application layers adapter-independent
BeforeLights Aug 2, 2026
a110846
fix(sa): preserve formula geometry and formatting gates
BeforeLights Aug 2, 2026
b85d843
fix(engine): satisfy deterministic processor lint gates
BeforeLights Aug 2, 2026
5459778
fix(engine): match Ruff processor formatting
BeforeLights Aug 2, 2026
688896a
Merge pull request #26 from DatabreezeService/feat/artifacts-datasets…
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
41 changes: 41 additions & 0 deletions docs/release-evidence/sa-spreadsheet-auditor-slice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Spreadsheet Auditor vertical-slice evidence

This record describes the implemented checkpoint on the Spreadsheet Auditor plan. It is
deliberately marked **partial**: it does not release SA-001..SA-027 or replace the full
module gate in `docs/plans/110-spreadsheet-auditor.md`.

## Included in this checkpoint

- Safe deterministic XLSX inventory and formula-family anomaly detection in
`services/engine/src/databreeze_engine/processors/spreadsheet_auditor.py`.
- A Python manifest bridge that adds server-issued opaque identities and tenant scope without
copying workbook values in
`services/engine/src/databreeze_engine/processors/spreadsheet_auditor_manifest.py`.
- The canonical value-free TypeScript result contract in
`packages/domain/src/spreadsheet-audit/v1.ts`.
- Tenant-scoped API registration, lookup, and artifact-version listing under
`/v1/spreadsheet-audits`.
- Immutable in-memory and Prisma adapters with the `sa` PostgreSQL schema and migration.
- Unknown-field rejection tests proving formulas, source values, and raw rows cannot enter the
HTTP result boundary.

## Evidence collected

- Domain build, public API smoke test, and domain test suite pass.
- API typecheck, API test compilation, targeted API controller/adapter tests, OpenAPI generation,
Prisma validation/generation, and migration inventory checks pass.
- Python sources and tests pass `python -m py_compile`.

The Python `uv` test command remains blocked by the existing Windows engine environment: the
checked-in `.venv\Scripts\python.exe` exits with `0xc0e90002` before pytest starts. Recreate or
repair that environment in a dedicated follow-up task; do not mark the engine requirement
verified from the compile-only result.

## Safety and rollback

- The parser rejects archive traversal, duplicate members, XML entity/DTD payloads, and resource
exhaustion; macros and external links are disclosed as blocked reasons and never executed.
- Result persistence is immutable and tenant scoped. Replaying the same audit ID is idempotent;
conflicting content fails closed.
- The slice is independently reversible through the commits on
`feat/artifacts-datasets-completion`; the next integration step is a reviewed PR to `dev`.
32 changes: 32 additions & 0 deletions packages/domain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@
"types": "./src/artifact-governance/v1.ts",
"import": "./dist/artifact-governance/v1.js"
},
"./artifact-retention/v1": {
"types": "./src/artifact-retention/v1.ts",
"import": "./dist/artifact-retention/v1.js"
},
"./artifact-export/v1": {
"types": "./src/artifact-export/v1.ts",
"import": "./dist/artifact-export/v1.js"
},
"./artifact-upload/v1": {
"types": "./src/artifact-upload/v1.ts",
"import": "./dist/artifact-upload/v1.js"
},
"./protected-document/v1": {
"types": "./src/protected-document/v1.ts",
"import": "./dist/protected-document/v1.js"
},
"./dataset/v1": {
"types": "./src/dataset/v1.ts",
"import": "./dist/dataset/v1.js"
Expand All @@ -80,6 +96,22 @@
"types": "./src/dataset-governance/v1.ts",
"import": "./dist/dataset-governance/v1.js"
},
"./dataset-quality/v1": {
"types": "./src/dataset-quality/v1.ts",
"import": "./dist/dataset-quality/v1.js"
},
"./dataset-profile/v1": {
"types": "./src/dataset-profile/v1.ts",
"import": "./dist/dataset-profile/v1.js"
},
"./dataset-export/v1": {
"types": "./src/dataset-export/v1.ts",
"import": "./dist/dataset-export/v1.js"
},
"./spreadsheet-audit/v1": {
"types": "./src/spreadsheet-audit/v1.ts",
"import": "./dist/spreadsheet-audit/v1.js"
},
"./jobs/v1": {
"types": "./src/jobs/v1.ts",
"import": "./dist/jobs/v1.js"
Expand Down
159 changes: 159 additions & 0 deletions packages/domain/src/artifact-export/v1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import {
parseStableIdentifierV1,
parseStrictUtcTimestampV1,
parseTenantScopeV1,
tenantScopesEqualV1,
type StableIdentifierV1,
type StrictUtcTimestampV1,
type TenantScopeV1,
} from '../tenant-scope/v1.js';

/** IAE-018: independent verification manifest for governed artifact exports. */
export const ARTIFACT_EXPORT_SCHEMA_VERSION_V1 = 1 as const;

export type ExportApprovalStateV1 = 'NOT_REQUIRED' | 'PENDING' | 'APPROVED' | 'REJECTED';

export interface ArtifactExportEntryV1 {
readonly versionId: StableIdentifierV1;
readonly contentSha256: string;
readonly byteSize: number;
readonly evidenceIds: readonly StableIdentifierV1[];
readonly processorVersions: readonly string[];
}

export interface ArtifactExportManifestV1 {
readonly schemaVersion: typeof ARTIFACT_EXPORT_SCHEMA_VERSION_V1;
readonly manifestId: StableIdentifierV1;
readonly tenantScope: TenantScopeV1;
readonly entries: readonly ArtifactExportEntryV1[];
readonly approvalState: ExportApprovalStateV1;
readonly createdAt: StrictUtcTimestampV1;
readonly canonicalHash: string;
}

export type ArtifactExportErrorCodeV1 =
| 'INVALID_IDENTIFIER'
| 'INVALID_SCOPE'
| 'CROSS_SCOPE'
| 'INVALID_TIMESTAMP'
| 'INVALID_HASH'
| 'INVALID_ENTRY'
| 'DUPLICATE_IDENTIFIER'
| 'INVALID_APPROVAL';

export type ArtifactExportResultV1<TValue> =
| { readonly accepted: true; readonly value: TValue }
| { readonly accepted: false; readonly code: ArtifactExportErrorCodeV1 };

function accepted<TValue>(value: TValue): ArtifactExportResultV1<TValue> {
return Object.freeze({ accepted: true, value });
}

function rejected(code: ArtifactExportErrorCodeV1): ArtifactExportResultV1<never> {
return Object.freeze({ accepted: false, code });
}

function identifier(input: unknown): StableIdentifierV1 | undefined {
const result = parseStableIdentifierV1(input);
return result.accepted ? result.value : undefined;
}

function timestamp(input: unknown): StrictUtcTimestampV1 | undefined {
const result = parseStrictUtcTimestampV1(input);
return result.accepted ? result.value : undefined;
}

function text(input: unknown): string | undefined {
return typeof input === 'string' &&
input.length > 0 &&
input.length <= 128 &&
!/\p{Cc}/u.test(input)
? input.normalize('NFC').trim()
: undefined;
}

export function createArtifactExportManifestV1(input: {
readonly manifestId: unknown;
readonly tenantScope: unknown;
readonly entries: unknown;
readonly approvalState: unknown;
readonly createdAt: unknown;
readonly canonicalHash: unknown;
}): ArtifactExportResultV1<ArtifactExportManifestV1> {
const manifestId = identifier(input.manifestId);
const tenantScope = parseTenantScopeV1(input.tenantScope);
const createdAt = timestamp(input.createdAt);
const canonicalHash =
typeof input.canonicalHash === 'string' && /^[0-9a-f]{64}$/u.test(input.canonicalHash)
? input.canonicalHash.toLowerCase()
: undefined;
if (!manifestId) return rejected('INVALID_IDENTIFIER');
if (!tenantScope.accepted) return rejected('INVALID_SCOPE');
if (!createdAt) return rejected('INVALID_TIMESTAMP');
if (!canonicalHash) return rejected('INVALID_HASH');
if (!['NOT_REQUIRED', 'PENDING', 'APPROVED', 'REJECTED'].includes(input.approvalState as string))
return rejected('INVALID_APPROVAL');
if (!Array.isArray(input.entries) || input.entries.length === 0 || input.entries.length > 1024)
return rejected('INVALID_ENTRY');
const entries: ArtifactExportEntryV1[] = [];
for (const candidate of input.entries) {
if (typeof candidate !== 'object' || candidate === null || Array.isArray(candidate))
return rejected('INVALID_ENTRY');
const record = candidate as Record<string, unknown>;
const versionId = identifier(record['versionId']);
const contentSha256 =
typeof record['contentSha256'] === 'string' && /^[0-9a-f]{64}$/u.test(record['contentSha256'])
? record['contentSha256'].toLowerCase()
: undefined;
const byteSize = record['byteSize'];
const evidenceIds = Array.isArray(record['evidenceIds'])
? record['evidenceIds'].map(identifier)
: undefined;
const processorVersions = Array.isArray(record['processorVersions'])
? record['processorVersions'].map(text)
: undefined;
if (
!versionId ||
!contentSha256 ||
typeof byteSize !== 'number' ||
!Number.isSafeInteger(byteSize) ||
byteSize < 0 ||
!evidenceIds ||
evidenceIds.some((value): value is undefined => value === undefined) ||
!processorVersions ||
processorVersions.some((value): value is undefined => value === undefined)
)
return rejected('INVALID_ENTRY');
entries.push(
Object.freeze({
versionId,
contentSha256,
byteSize,
evidenceIds: Object.freeze(evidenceIds as StableIdentifierV1[]),
processorVersions: Object.freeze(processorVersions as string[]),
}),
);
}
if (new Set(entries.map((entry) => entry.versionId)).size !== entries.length)
return rejected('DUPLICATE_IDENTIFIER');
if (entries.some((entry) => entry.evidenceIds.some((evidenceId) => !evidenceId)))
return rejected('INVALID_ENTRY');
return accepted(
Object.freeze({
schemaVersion: ARTIFACT_EXPORT_SCHEMA_VERSION_V1,
manifestId,
tenantScope: tenantScope.value,
entries: Object.freeze(entries),
approvalState: input.approvalState as ExportApprovalStateV1,
createdAt,
canonicalHash,
}),
);
}

export function exportScopesEqualV1(
left: ArtifactExportManifestV1,
right: ArtifactExportManifestV1,
): boolean {
return tenantScopesEqualV1(left.tenantScope, right.tenantScope);
}
75 changes: 74 additions & 1 deletion packages/domain/src/artifact-intake/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type InboxItemStateV1 =
| 'QUARANTINED'
| 'ARCHIVED';
export type ArtifactScanStateV1 = 'PENDING' | 'CLEAN' | 'MALICIOUS' | 'FAILED';
export type InboxPriorityV1 = 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT';

export interface InboxItemV1 {
readonly schemaVersion: typeof ARTIFACT_INTAKE_SCHEMA_VERSION_V1;
Expand All @@ -31,6 +32,10 @@ export interface InboxItemV1 {
readonly state: InboxItemStateV1;
readonly createdAt: StrictUtcTimestampV1;
readonly revision: number;
readonly assigneeId?: StableIdentifierV1;
readonly labels?: readonly string[];
readonly priority?: InboxPriorityV1;
readonly dueAt?: StrictUtcTimestampV1;
}

export type ArtifactIntakeErrorCodeV1 =
Expand All @@ -47,7 +52,9 @@ export type ArtifactIntakeErrorCodeV1 =
| 'SIZE_MISMATCH'
| 'MEDIA_MISMATCH'
| 'SIZE_POLICY_EXCEEDED'
| 'SCAN_NOT_COMPLETE';
| 'SCAN_NOT_COMPLETE'
| 'INVALID_METADATA'
| 'REVISION_CONFLICT';

export type ArtifactIntakeResultV1<TValue> =
| { readonly accepted: true; readonly value: TValue }
Expand Down Expand Up @@ -146,6 +153,72 @@ export function transitionInboxItemV1(
return accepted(Object.freeze({ ...item, state: nextState, revision: item.revision + 1 }));
}

/** IAE-013: metadata updates are revisioned and never change artifact identity or state. */
export function updateInboxMetadataV1(
item: InboxItemV1,
input: {
readonly assigneeId?: unknown;
readonly labels?: unknown;
readonly priority?: unknown;
readonly dueAt?: unknown;
readonly expectedRevision: unknown;
},
): ArtifactIntakeResultV1<InboxItemV1> {
if (
typeof input.expectedRevision !== 'number' ||
!Number.isSafeInteger(input.expectedRevision) ||
input.expectedRevision < 1
)
return rejected('INVALID_METADATA');
if (input.expectedRevision !== item.revision) return rejected('REVISION_CONFLICT');
let assigneeId = item.assigneeId;
if (input.assigneeId !== undefined) {
if (input.assigneeId === null) assigneeId = undefined;
else {
assigneeId = identifier(input.assigneeId);
if (!assigneeId) return rejected('INVALID_METADATA');
}
}
let labels = item.labels;
if (input.labels !== undefined) {
if (!Array.isArray(input.labels) || input.labels.length > 32)
return rejected('INVALID_METADATA');
const parsedLabels = input.labels.map((label) => text(label, 64));
if (
parsedLabels.some((label): label is undefined => label === undefined) ||
new Set(parsedLabels).size !== parsedLabels.length
)
return rejected('INVALID_METADATA');
labels = Object.freeze(parsedLabels as string[]);
}
let priority = item.priority;
if (input.priority !== undefined) {
if (!['LOW', 'NORMAL', 'HIGH', 'URGENT'].includes(input.priority as string))
return rejected('INVALID_METADATA');
priority = input.priority as InboxPriorityV1;
}
let dueAt = item.dueAt;
if (input.dueAt !== undefined) {
if (input.dueAt === null) dueAt = undefined;
else {
dueAt = timestamp(input.dueAt);
if (!dueAt) return rejected('INVALID_METADATA');
}
}
const next = { ...item, revision: item.revision + 1 };
if (input.assigneeId !== undefined) {
if (assigneeId === undefined) delete next.assigneeId;
else next.assigneeId = assigneeId;
}
if (labels !== undefined) next.labels = labels;
if (priority !== undefined) next.priority = priority;
if (input.dueAt !== undefined) {
if (dueAt === undefined) delete next.dueAt;
else next.dueAt = dueAt;
}
return accepted(Object.freeze(next));
}

export function finalizeArtifactAdmissionV1(input: {
readonly artifact: ArtifactVersionV1;
readonly actualSha256: unknown;
Expand Down
Loading
Loading