docs(evidence): publish curated-only workspace contract
This commit is contained in:
@@ -226,6 +226,13 @@
|
|||||||
- **Engine:** `evidencePolicy` no longer stops filesystem sources (`evidence_materialization_required`
|
- **Engine:** `evidencePolicy` no longer stops filesystem sources (`evidence_materialization_required`
|
||||||
retired); `preprocess evidence`/`preprocess run` operate on the materialized root. Evidence Qdrant
|
retired); `preprocess evidence`/`preprocess run` operate on the materialized root. Evidence Qdrant
|
||||||
records remain revision-scoped; corpus ACTIVE is revision-qualified. HTTP/S3 Evidence is unchanged.
|
records remain revision-scoped; corpus ACTIVE is revision-qualified. HTTP/S3 Evidence is unchanged.
|
||||||
|
- **Curated-only runtime contract (Evidence schema v2):** the new authoring layout preserves the
|
||||||
|
complete commit-addressed `evidence/` tree (`source/`, `curated/`, manifest and evaluation files),
|
||||||
|
while the rendered filesystem acquisition default is only `curated/**/*.md`. Version 2 rejects
|
||||||
|
source or mixed source/curated runtime patterns; legacy Evidence version 1 retains its explicit
|
||||||
|
safe-pattern compatibility. The curator validates before merge and the runtime validates the
|
||||||
|
pinned curated corpus before indexing. The existing unnamed dense vector remains intact while
|
||||||
|
Evidence may add `bm25`/`idf` additively; no runtime operation writes the authoring repository.
|
||||||
- **Retention:** materialized roots live inside the commit-addressed snapshot directory, so they are
|
- **Retention:** materialized roots live inside the commit-addressed snapshot directory, so they are
|
||||||
retained while pinned and removed by the existing snapshot retention scan when unreferenced.
|
retained while pinned and removed by the existing snapshot retention scan when unreferenced.
|
||||||
- **Key files:** `backend/src/workspaces/evidence-materialization.ts` (+test),
|
- **Key files:** `backend/src/workspaces/evidence-materialization.ts` (+test),
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export type EvidenceSource =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface WorkspaceEvidence {
|
export interface WorkspaceEvidence {
|
||||||
|
schema_version: 1 | 2;
|
||||||
source: EvidenceSource;
|
source: EvidenceSource;
|
||||||
policy: EvidencePolicy;
|
policy: EvidencePolicy;
|
||||||
}
|
}
|
||||||
@@ -246,10 +247,10 @@ const evidencePattern = z.string().refine(isSafeEvidencePattern, {
|
|||||||
const filesystemEvidenceSourceSchema = z.object({
|
const filesystemEvidenceSourceSchema = z.object({
|
||||||
type: z.literal("filesystem"),
|
type: z.literal("filesystem"),
|
||||||
uri: z.string(),
|
uri: z.string(),
|
||||||
patterns: z.array(evidencePattern).min(1).default(["**/*.md"]),
|
patterns: z.array(evidencePattern).min(1).optional(),
|
||||||
max_bytes: positiveSafeInteger.default(10 * 1024 * 1024),
|
max_bytes: positiveSafeInteger.default(10 * 1024 * 1024),
|
||||||
}).strict().superRefine((source, context) => {
|
}).strict().superRefine((source, context) => {
|
||||||
if (new Set(source.patterns).size !== source.patterns.length) {
|
if (source.patterns !== undefined && new Set(source.patterns).size !== source.patterns.length) {
|
||||||
context.addIssue({ code: "custom", path: ["patterns"], message: "evidence patterns must not repeat" });
|
context.addIssue({ code: "custom", path: ["patterns"], message: "evidence patterns must not repeat" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -323,12 +324,39 @@ const evidencePolicySchema = z.object({
|
|||||||
retain_published_generations: positiveSafeInteger.default(3),
|
retain_published_generations: positiveSafeInteger.default(3),
|
||||||
}).strict();
|
}).strict();
|
||||||
const workspaceEvidenceSchema = z.object({
|
const workspaceEvidenceSchema = z.object({
|
||||||
|
schema_version: z.union([z.literal(1), z.literal(2)]).default(1),
|
||||||
source: evidenceSourceSchema,
|
source: evidenceSourceSchema,
|
||||||
policy: evidencePolicySchema.default({
|
policy: evidencePolicySchema.default({
|
||||||
max_chunk_chars: 4_000,
|
max_chunk_chars: 4_000,
|
||||||
retain_published_generations: 3,
|
retain_published_generations: 3,
|
||||||
}),
|
}),
|
||||||
}).strict();
|
}).strict().superRefine((evidence, context) => {
|
||||||
|
if (evidence.schema_version !== 2 || evidence.source.type !== "filesystem") return;
|
||||||
|
const patterns = evidence.source.patterns ?? ["curated/**/*.md"];
|
||||||
|
const selectsSource = patterns.some((pattern) => pattern === "source" || pattern.startsWith("source/"));
|
||||||
|
const selectsCurated = patterns.some((pattern) => pattern === "curated" || pattern.startsWith("curated/"));
|
||||||
|
if (selectsSource && selectsCurated) {
|
||||||
|
context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
path: ["source", "patterns"],
|
||||||
|
message: "schema-versioned filesystem Evidence patterns cannot span source and curated",
|
||||||
|
});
|
||||||
|
} else if (!patterns.every((pattern) => pattern === "curated" || pattern.startsWith("curated/"))) {
|
||||||
|
context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
path: ["source", "patterns"],
|
||||||
|
message: "schema-versioned filesystem Evidence patterns must acquire curated documents only",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).transform((evidence) => ({
|
||||||
|
...evidence,
|
||||||
|
source: evidence.source.type !== "filesystem" || evidence.source.patterns !== undefined
|
||||||
|
? evidence.source
|
||||||
|
: {
|
||||||
|
...evidence.source,
|
||||||
|
patterns: evidence.schema_version === 2 ? ["curated/**/*.md"] : ["**/*.md"],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
function unique<T>(values: readonly T[], context: z.RefinementCtx, path: PropertyKey[]) {
|
function unique<T>(values: readonly T[], context: z.RefinementCtx, path: PropertyKey[]) {
|
||||||
if (new Set(values).size !== values.length) {
|
if (new Set(values).size !== values.length) {
|
||||||
|
|||||||
@@ -139,8 +139,14 @@ function evidenceSecretFile(name: string, contents: string): string {
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
function evidenceWorkspace(source: Record<string, unknown>, policy?: Record<string, unknown>) {
|
function evidenceWorkspace(
|
||||||
return parseWorkspaceYaml(`${canonicalEvidenceWorkspace}\nevidence:\n source: ${JSON.stringify(source)}${
|
source: Record<string, unknown>,
|
||||||
|
policy?: Record<string, unknown>,
|
||||||
|
evidenceSchemaVersion?: number,
|
||||||
|
) {
|
||||||
|
return parseWorkspaceYaml(`${canonicalEvidenceWorkspace}\nevidence:${
|
||||||
|
evidenceSchemaVersion === undefined ? "" : `\n schema_version: ${evidenceSchemaVersion}`
|
||||||
|
}\n source: ${JSON.stringify(source)}${
|
||||||
policy === undefined ? "" : `\n policy: ${JSON.stringify(policy)}`
|
policy === undefined ? "" : `\n policy: ${JSON.stringify(policy)}`
|
||||||
}\n`);
|
}\n`);
|
||||||
}
|
}
|
||||||
@@ -180,9 +186,10 @@ function evidenceRender(
|
|||||||
source: Record<string, unknown>,
|
source: Record<string, unknown>,
|
||||||
evidenceBinding: RuntimeBindings["evidence"] = { missing: [], values: {} },
|
evidenceBinding: RuntimeBindings["evidence"] = { missing: [], values: {} },
|
||||||
policy?: Record<string, unknown>,
|
policy?: Record<string, unknown>,
|
||||||
|
evidenceSchemaVersion?: number,
|
||||||
) {
|
) {
|
||||||
return renderRuntimeConfig(
|
return renderRuntimeConfig(
|
||||||
evidenceWorkspace(source, policy),
|
evidenceWorkspace(source, policy, evidenceSchemaVersion),
|
||||||
{ ...directBindings, evidence: evidenceBinding },
|
{ ...directBindings, evidence: evidenceBinding },
|
||||||
paths,
|
paths,
|
||||||
evidenceContext,
|
evidenceContext,
|
||||||
@@ -195,7 +202,7 @@ test("renders filesystem Evidence below the immutable revision content root with
|
|||||||
const yaml = evidenceRender({
|
const yaml = evidenceRender({
|
||||||
type: "filesystem",
|
type: "filesystem",
|
||||||
uri: "psd-clinical/evidence",
|
uri: "psd-clinical/evidence",
|
||||||
});
|
}, undefined, undefined, 2);
|
||||||
const rendered = parse(yaml);
|
const rendered = parse(yaml);
|
||||||
|
|
||||||
expect(rendered.runtime_identity.workspace_revision).toBe(evidenceRevision);
|
expect(rendered.runtime_identity.workspace_revision).toBe(evidenceRevision);
|
||||||
@@ -203,7 +210,7 @@ test("renders filesystem Evidence below the immutable revision content root with
|
|||||||
sources: [{
|
sources: [{
|
||||||
type: "filesystem",
|
type: "filesystem",
|
||||||
root: `/srv/registry/snapshots/${evidenceRevision}/psd-clinical/evidence`,
|
root: `/srv/registry/snapshots/${evidenceRevision}/psd-clinical/evidence`,
|
||||||
patterns: ["**/*.md"],
|
patterns: ["curated/**/*.md"],
|
||||||
max_bytes: 10_485_760,
|
max_bytes: 10_485_760,
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -390,6 +390,58 @@ test("applies filesystem and policy defaults to the canonical descriptor", () =>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("defaults schema-versioned filesystem Evidence to curated documents only", () => {
|
||||||
|
const parsed = validateWorkspaceDescriptor({
|
||||||
|
...withEvidence({
|
||||||
|
type: "filesystem",
|
||||||
|
uri: "psd-clinical/evidence",
|
||||||
|
}),
|
||||||
|
evidence: {
|
||||||
|
schema_version: 2,
|
||||||
|
source: { type: "filesystem", uri: "psd-clinical/evidence" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parsed.evidence).toMatchObject({
|
||||||
|
schema_version: 2,
|
||||||
|
source: { patterns: ["curated/**/*.md"] },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects a schema-versioned Evidence layout that mixes source and curated runtime patterns", () => {
|
||||||
|
expectSafeEvidenceError({
|
||||||
|
...withEvidence({
|
||||||
|
type: "filesystem",
|
||||||
|
uri: "psd-clinical/evidence",
|
||||||
|
}),
|
||||||
|
evidence: {
|
||||||
|
schema_version: 2,
|
||||||
|
source: {
|
||||||
|
type: "filesystem",
|
||||||
|
uri: "psd-clinical/evidence",
|
||||||
|
patterns: ["source/**/*.md", "curated/**/*.md"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, /source.*curated|curated.*source/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects a schema-versioned Evidence layout that acquires source documents at runtime", () => {
|
||||||
|
expectSafeEvidenceError({
|
||||||
|
...withEvidence({
|
||||||
|
type: "filesystem",
|
||||||
|
uri: "psd-clinical/evidence",
|
||||||
|
}),
|
||||||
|
evidence: {
|
||||||
|
schema_version: 2,
|
||||||
|
source: {
|
||||||
|
type: "filesystem",
|
||||||
|
uri: "psd-clinical/evidence",
|
||||||
|
patterns: ["source/**/*.md"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, /curated/i);
|
||||||
|
});
|
||||||
|
|
||||||
test("keeps evidence optional on schema v3", () => {
|
test("keeps evidence optional on schema v3", () => {
|
||||||
expect(validateWorkspaceDescriptor(validWorkspaceObject())).not.toHaveProperty("evidence");
|
expect(validateWorkspaceDescriptor(validWorkspaceObject())).not.toHaveProperty("evidence");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,6 +46,21 @@ Il modello propone; un revisore umano decide ai gate tramite widget:
|
|||||||
|
|
||||||
Il frontend renderizza questi widget-descriptor (registro in `src/widgets/`); il transcript live viene ricostruito in memoria dallo stream SSE (`src/store/sessionStore.ts`) — **non è persistito**.
|
Il frontend renderizza questi widget-descriptor (registro in `src/widgets/`); il transcript live viene ricostruito in memoria dallo stream SSE (`src/store/sessionStore.ts`) — **non è persistito**.
|
||||||
|
|
||||||
|
## Evidence curata e immutabile
|
||||||
|
|
||||||
|
Il repository del workspace è il confine di pubblicazione: il curatore prepara `evidence/source/`,
|
||||||
|
revisa le unità in `evidence/curated/`, valida e fa merge. Per `evidence.schema_version: 2` il
|
||||||
|
runtime materializza l'intero albero `evidence/` dal commit Git esatto, ma il renderer consegna al
|
||||||
|
preprocessing soltanto `curated/**/*.md` dalla root immutabile della revisione. Sorgenti, manifest
|
||||||
|
ed evaluation restano disponibili solo per tracciabilità. Il runtime non modifica, stagea, committa
|
||||||
|
o pubblica il repository di authoring.
|
||||||
|
|
||||||
|
Prima dell'indicizzazione, il corpus curato della revisione pinnata viene validato. La collezione
|
||||||
|
Qdrant condivisa conserva il vettore dense senza nome di Schema e Memory; il preprocessing Evidence
|
||||||
|
può aggiungere soltanto in modo additivo il vettore sparse `bm25` con `idf`, senza eliminare,
|
||||||
|
rinominare o ricreare la collezione. `workspace preprocess evidence` e la parte Evidence di
|
||||||
|
`workspace preprocess run` sono le sole operazioni pubbliche che effettuano questo upgrade.
|
||||||
|
|
||||||
## Punti di attenzione ricorrenti
|
## Punti di attenzione ricorrenti
|
||||||
|
|
||||||
- `tht -c`/`--config` è un'opzione **per-comando**: deve seguire il subcommand, mai precederlo (`ThtRunner.buildArgv` lo impone).
|
- `tht -c`/`--config` è un'opzione **per-comando**: deve seguire il subcommand, mai precederlo (`ThtRunner.buildArgv` lo impone).
|
||||||
|
|||||||
@@ -8,18 +8,40 @@ source variant and the policy reject unknown keys.
|
|||||||
## Filesystem source
|
## Filesystem source
|
||||||
|
|
||||||
A filesystem source uses the exact URI `<workspace.id>/evidence`. `patterns` is a nonempty list of
|
A filesystem source uses the exact URI `<workspace.id>/evidence`. `patterns` is a nonempty list of
|
||||||
unique, normalized relative POSIX globs. Its defaults are `patterns: ["**/*.md"]` and
|
unique, normalized relative POSIX globs. The Evidence-local `schema_version` defaults to `1` for
|
||||||
|
compatibility, where an omitted filesystem pattern defaults to `patterns: ["**/*.md"]` and
|
||||||
`max_bytes: 10485760`.
|
`max_bytes: 10485760`.
|
||||||
|
|
||||||
|
`evidence.schema_version: 2` declares the source/curated authoring layout. Its omitted filesystem
|
||||||
|
pattern defaults to `patterns: ["curated/**/*.md"]`; every explicit v2 filesystem pattern must also
|
||||||
|
remain below `curated/`. A v2 descriptor that selects `source/`, or spans both `source/` and
|
||||||
|
`curated/`, is rejected. Explicit safe legacy filesystem patterns remain supported under Evidence
|
||||||
|
version 1. HTTP and S3 sources do not use filesystem layout patterns and retain their existing
|
||||||
|
contracts.
|
||||||
|
|
||||||
|
The v2 authoring tree is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
evidence/
|
||||||
|
├── source/ # preserved original material
|
||||||
|
├── curated/ # reviewed Evidence Units indexed at runtime
|
||||||
|
├── manifest.yaml
|
||||||
|
└── evaluation.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
`source/`, the manifest, the evaluation set, and other support files are materialized for
|
||||||
|
traceability but never acquired by v2 runtime preprocessing.
|
||||||
|
|
||||||
### Example: filesystem
|
### Example: filesystem
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
evidence:
|
evidence:
|
||||||
|
schema_version: 2
|
||||||
source:
|
source:
|
||||||
type: filesystem
|
type: filesystem
|
||||||
uri: example/evidence
|
uri: example/evidence
|
||||||
patterns:
|
patterns:
|
||||||
- "**/*.md"
|
- "curated/**/*.md"
|
||||||
max_bytes: 10485760
|
max_bytes: 10485760
|
||||||
policy:
|
policy:
|
||||||
max_chunk_chars: 4000
|
max_chunk_chars: 4000
|
||||||
@@ -152,8 +174,10 @@ catalog metadata exactly. Every catalog entry must have its descriptor at that s
|
|||||||
catalog-only entries are invalid and reject the complete candidate revision.
|
catalog-only entries are invalid and reject the complete candidate revision.
|
||||||
|
|
||||||
Workspace source changes only through curator Git commit/push in a separate authoring clone,
|
Workspace source changes only through curator Git commit/push in a separate authoring clone,
|
||||||
followed by an installation pull. The API never writes `thoth-workspaces.yaml`,
|
followed by an installation pull. Curator validation occurs before merge; activation and
|
||||||
`<id>/workspace.yaml`, `<id>/schema/**`, or `<id>/evidence/**`.
|
preprocessing consume only the merged, pinned commit. The API and runtime never write
|
||||||
|
`thoth-workspaces.yaml`, `<id>/workspace.yaml`, `<id>/schema/**`, or `<id>/evidence/**` in the
|
||||||
|
authoring repository.
|
||||||
|
|
||||||
## Registry revision and phase ownership
|
## Registry revision and phase ownership
|
||||||
|
|
||||||
@@ -164,7 +188,7 @@ followed by an installation pull. The API never writes `thoth-workspaces.yaml`,
|
|||||||
| Repository consumer | ThothII fetches and validates a complete candidate, atomically activates it only on success, and never edits, commits, or pushes repository content. |
|
| Repository consumer | ThothII fetches and validates a complete candidate, atomically activates it only on success, and never edits, commits, or pushes repository content. |
|
||||||
| Runtime secrets | Workspace management returns configured/missing status only; decrypted values exist only for the lifetime of a diagnostic or runtime lease. |
|
| Runtime secrets | Workspace management returns configured/missing status only; decrypted values exist only for the lifetime of a diagnostic or runtime lease. |
|
||||||
| P1.1 | Validates the lexical URI `<id>/evidence` and proves the declared filesystem root object is a Git tree at that same commit; it does not recursively inspect nested symlinks. Evidence materialization stays out of scope for P1.1. |
|
| P1.1 | Validates the lexical URI `<id>/evidence` and proves the declared filesystem root object is a Git tree at that same commit; it does not recursively inspect nested symlinks. Evidence materialization stays out of scope for P1.1. |
|
||||||
| P6 | Owns commit-addressed materialization, realpath and recursive containment, nested-symlink checks, and race checks. |
|
| P6 | Owns commit-addressed materialization of the complete Evidence tree, realpath and recursive containment, nested-symlink checks, and race checks. |
|
||||||
|
|
||||||
P1.1 performs no acquisition, extraction, preprocessing/indexing, embeddings, Qdrant writes,
|
P1.1 performs no acquisition, extraction, preprocessing/indexing, embeddings, Qdrant writes,
|
||||||
active-snapshot retention, or GC.
|
active-snapshot retention, or GC.
|
||||||
|
|||||||
@@ -106,7 +106,13 @@ tht --installation <absolute>/thothii-installation.yaml workspace vector rebuild
|
|||||||
before any bytes are written and no partial root is published.
|
before any bytes are written and no partial root is published.
|
||||||
- `preprocess evidence` and `preprocess run` operate directly on the materialized root; the
|
- `preprocess evidence` and `preprocess run` operate directly on the materialized root; the
|
||||||
temporary `evidence_materialization_required` stop is retired (the code remains only for
|
temporary `evidence_materialization_required` stop is retired (the code remains only for
|
||||||
pre-P6 compatibility). HTTP/S3 Evidence is unchanged.
|
pre-P6 compatibility). For `evidence.schema_version: 2`, runtime acquisition receives only
|
||||||
|
`curated/**/*.md`; `source/` and support files remain in the materialized tree for traceability.
|
||||||
|
HTTP/S3 Evidence is unchanged.
|
||||||
|
- The curator validates Evidence before merge. Preprocessing validates the pinned curated corpus
|
||||||
|
again before it constructs a candidate generation, so an invalid revision is never indexed.
|
||||||
|
- The runtime writes only its immutable materialized snapshot and derived index state. It never
|
||||||
|
writes, stages, commits, or pushes the workspace authoring repository.
|
||||||
- Materialized roots are retained with their commit-addressed snapshot directory and removed only
|
- Materialized roots are retained with their commit-addressed snapshot directory and removed only
|
||||||
when the revision becomes unreferenced.
|
when the revision becomes unreferenced.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user