diff --git a/integration/tooling.md b/integration/tooling.md index 11db2935..f0ae3b36 100644 --- a/integration/tooling.md +++ b/integration/tooling.md @@ -20,7 +20,7 @@ Transitrix separates validation into two canonical axes (file scope and repo sco |---|---|---|---|---| | `npx @transitrix/cli validate ` | **File** — one `*.transitrix.yaml` at a time | Schema, required fields, notation-specific rules for all 15 canonical extensions (no `--ext` flag needed for canonical notations) | On every save; pre-commit hook; CI per-file job | **Active — canonical** | | `python3 .validators/lint.py` | **Repo** — whole `canon/` tree at once | Atomicity (no relations inside element files), referential integrity, ArchiMate semantics, policy (Active status requires an owner) | Before every PR; CI repo-integrity job | **Active — canonical.** Fetched from `tools/lint.py` at scaffold time; lives at `.validators/lint.py` in the adopter repo | -| `python3 tools/knowledge_store_lint.py ` | **Knowledge store** — `_intake/processed/` + `_intake/drafts/` + `knowledge/` | Quality Gates 1–6: quarantine, canonical-definition, blast-radius tiering, provenance/confidence, scoped consistency, assisted ingest — per `patterns/knowledge-store.md` | When proposing or promoting knowledge objects; when reviewing the store for drift | **Methodology-internal reference implementation.** Adopters may copy and adapt it; the methodology owns the rules, not this script. Documented in [`patterns/knowledge-store.md`](../patterns/knowledge-store.md) | +| `python3 tools/knowledge_store_lint.py ` | **Knowledge store** — `sources/` (and legacy `_intake/processed/`) + `_intake/drafts/` + `knowledge/` | Quality Gates 1–6: quarantine, canonical-definition, blast-radius tiering, provenance/confidence, scoped consistency, assisted ingest — per `patterns/knowledge-store.md` | When proposing or promoting knowledge objects; when reviewing the store for drift | **Methodology-internal reference implementation.** Adopters may copy and adapt it; the methodology owns the rules, not this script. Documented in [`patterns/knowledge-store.md`](../patterns/knowledge-store.md) | | `python3 tools/check_views_compliance.py` | Methodology-repo only | Notation-specific rules on the methodology's own example files | Not for adopters | **Deprecated** — superseded by `@transitrix/cli`. Kept for migration reference only | ### Requirement-chain consumer support diff --git a/packages/ingest-cli/src/intake-profile.mjs b/packages/ingest-cli/src/intake-profile.mjs index 33bb8501..8c2a27c5 100644 --- a/packages/ingest-cli/src/intake-profile.mjs +++ b/packages/ingest-cli/src/intake-profile.mjs @@ -1,6 +1,6 @@ // Skill-local deployment compatibility. Read-only checks precede every setup write. import { lstat, readdir, readFile, mkdir, writeFile } from 'node:fs/promises'; -import { join, resolve, relative } from 'node:path'; +import { basename, join, resolve, relative } from 'node:path'; import { spawnSync } from 'node:child_process'; const PROFILES = ['ingest', 'knowledge-store']; @@ -33,6 +33,16 @@ async function files(path) { return result; } +async function sourceRecordType(path, legacy = false) { + const text = path.endsWith('.md') ? await readFile(path, 'utf8') : ''; + const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text)?.[1] || ''; + const match = /^(?:type|["']type["']):[ \t]*(["']?)(source-document|source-redirect)\1[ \t]*(?:#.*)?$/m.exec(frontmatter); + if (legacy && !match && /source-(?:document|redirect)/.test(frontmatter)) { + fail(`ambiguous source record at ${path}`); + } + return match?.[2] ?? null; +} + export async function checkIntakeProfile(orgRoot, profile) { if (!PROFILES.includes(profile)) throw new Error('Choose --profile ingest or --profile knowledge-store'); const root = resolve(orgRoot); @@ -58,12 +68,20 @@ export async function checkIntakeProfile(orgRoot, profile) { if (stat && !stat.isDirectory()) fail(`_intake/${name} is not a directory`); } const processed = await files(join(root, '_intake/processed')); + const sourceDir = join(root, 'sources'); + const sourceDirStat = await inspect(sourceDir); + if (profile === 'knowledge-store' && sourceDirStat && !sourceDirStat.isDirectory()) fail('sources is not a directory'); + const sourceFiles = sourceDirStat?.isDirectory() + ? (await files(sourceDir)).filter(path => path.endsWith('.md') && !['index.md', 'log.md'].includes(basename(path))) + : []; + for (const path of sourceFiles) { + if (await sourceRecordType(path) === 'source-document') { + evidence['knowledge-store'].push(relative(root, path)); + } + } for (const path of processed) { - const text = path.endsWith('.md') ? await readFile(path, 'utf8') : ''; - const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text)?.[1] || ''; - const okf = /^(?:type|["']type["']):[ \t]*(?:"source-document"|'source-document'|source-document)[ \t]*(?:#.*)?$/m.test(frontmatter); - if (!okf && frontmatter.includes('source-document')) fail(`ambiguous source record at ${relative(root, path)}`); - evidence[okf ? 'knowledge-store' : 'ingest'].push(relative(root, path)); + const type = await sourceRecordType(path, true); + evidence[type ? 'knowledge-store' : 'ingest'].push(relative(root, path)); } // Unknown workspace entries must be classified by the adopter, never guessed. if (intake) { @@ -85,7 +103,7 @@ export async function checkIntakeProfile(orgRoot, profile) { if (profile === 'knowledge-store') { // Git evaluates nested/global ignores and negations, including tracked files. - const paths = ['_intake/processed/profile-check.md', ...processed.map(p => relative(root, p))]; + const paths = ['sources/profile-check.md', ...[...sourceFiles, ...processed].map(p => relative(root, p))]; const result = spawnSync('git', ['-C', root, 'check-ignore', '--no-index', '--stdin'], { input: paths.join('\n') + '\n', encoding: 'utf8', }); diff --git a/packages/ingest-cli/src/intake-profile.test.mjs b/packages/ingest-cli/src/intake-profile.test.mjs index e0145194..952ebe4e 100644 --- a/packages/ingest-cli/src/intake-profile.test.mjs +++ b/packages/ingest-cli/src/intake-profile.test.mjs @@ -69,6 +69,7 @@ test('OKF selection refuses ingest setup and preserves source and original bytes for (const [path, content, requested] of [ ['_intake/processed/source.md', okf, 'ingest'], + ['sources/source.md', okf, 'ingest'], ['knowledge/index.md', '# Knowledge', 'ingest'], ['_intake/originals/source.txt', 'original', 'ingest'], ['_intake/drafts/draft.md', 'draft', 'ingest'], @@ -105,12 +106,34 @@ for (const [path, ignore] of [ ['.git/info/exclude', '_intake/processed/**\n'], ]) test(`OKF refuses effective ignore rules in ${path}`, async t => { const root = await workspace(t); + await put(root, '_intake/processed/source.md', okf); await put(root, path, ignore); const before = await snapshot(root); await assert.rejects(selectIntakeProfile(root, 'knowledge-store'), /profile conflict/); assert.deepEqual(await snapshot(root), before); }); +test('new layout checks source cards without reserving an unused legacy directory', async t => { + const root = await workspace(t); + await put(root, '.gitignore', '_intake/processed/\n'); + await checkIntakeProfile(root, 'knowledge-store'); + await put(root, 'sources/team/source.md', okf); + await checkIntakeProfile(root, 'knowledge-store'); + await put(root, 'sources/team/.gitignore', '*.md\n'); + await assert.rejects(checkIntakeProfile(root, 'knowledge-store'), /would be ignored/); +}); + +test('legacy redirects count as Knowledge Store evidence and remain visible', async t => { + const root = await workspace(t); + await put(root, 'sources/source.md', okf); + await put(root, '_intake/processed/source.md', + '---\ntype: source-redirect\nredirect_to: /sources/source.md\n---\n'); + await checkIntakeProfile(root, 'knowledge-store'); + await assert.rejects(checkIntakeProfile(root, 'ingest'), /knowledge-store evidence/); + await put(root, '.gitignore', '_intake/processed/\n'); + await assert.rejects(checkIntakeProfile(root, 'knowledge-store'), /would be ignored/); +}); + test('Git negation permits OKF; ignored tracked records are still refused', async t => { const root = await workspace(t); await put(root, '.gitignore', '_intake/processed/*\n!_intake/processed/*.md\n'); diff --git a/patterns/knowledge-store-templates/okf-knowledge-object-draft.md b/patterns/knowledge-store-templates/okf-knowledge-object-draft.md index ec33f952..967ed36d 100644 --- a/patterns/knowledge-store-templates/okf-knowledge-object-draft.md +++ b/patterns/knowledge-store-templates/okf-knowledge-object-draft.md @@ -3,7 +3,7 @@ type: insight | finding | concept | question title: "" description: "" resource: "" -source: /_intake/processed/source-document-name.md +source: /sources/source-document-name.md created_at: YYYY-MM-DD confidence: observed | inferred | assumed mapping: confirms | extends | proposes | conflicts # optional — Gate 2 classification @@ -22,7 +22,7 @@ _Full description of the knowledge object._ # Citations -- [Source document title](/_intake/processed/source-document-name.md) +- [Source document title](/sources/source-document-name.md) # Examples diff --git a/patterns/knowledge-store-templates/okf-knowledge-object.md b/patterns/knowledge-store-templates/okf-knowledge-object.md index b6ca62fc..6341cb1e 100644 --- a/patterns/knowledge-store-templates/okf-knowledge-object.md +++ b/patterns/knowledge-store-templates/okf-knowledge-object.md @@ -3,7 +3,7 @@ type: insight | finding | concept | question title: "" description: "" resource: "" -source: /intake/processed/source-document-name.md +source: /sources/source-document-name.md created_at: YYYY-MM-DD confidence: observed | inferred | assumed mapping: confirms | extends | proposes | conflicts # optional — Gate 2 classification when admitting @@ -21,7 +21,7 @@ _Full description of the knowledge object._ # Citations -- [Source document title](/intake/processed/source-document-name.md) +- [Source document title](/sources/source-document-name.md) # Examples diff --git a/patterns/knowledge-store.md b/patterns/knowledge-store.md index 90d64bc3..46062015 100644 --- a/patterns/knowledge-store.md +++ b/patterns/knowledge-store.md @@ -15,10 +15,10 @@ Transitrix includes an optional knowledge store component for organisations that Choose one intake profile per repository/workspace before setup: -| Profile | Consumer | `_intake/processed/` ownership and retention | +| Profile | Consumer | Source record and original locations | |---|---|---| | `ingest` | Ordinary ingest and reg-intel, together | Completed raw source files, private/gitignored | -| `knowledge-store` | OKF curation | Committed OKF `source-document` records; raw originals stay in gitignored `_intake/originals/` | +| `knowledge-store` | OKF curation | Committed OKF `source-document` records in `sources/`. Uploaded originals stay in gitignored `_intake/originals/`. | These profiles are **not supported together in one workspace**. Use separate workspaces if both are needed. This is a deployment boundary, not a migration or @@ -43,10 +43,10 @@ manifest field, coverage profile or permission grant. Re-run the check before subsequent intake writes; the marker does not override conflicting evidence. Detection checks the marker, profile-specific directories, the regulatory watch -list, and processed content (OKF source-document frontmatter versus raw files). +list, `sources/` records, and legacy processed content (OKF records or redirects versus raw files). Mixed evidence, unknown intake entries, symlinks or unreadable evidence stop setup for adopter inspection. The OKF check also uses Git's effective ignore rules, -including nested/global rules and tracked files, to refuse hidden source records. +including nested/global rules and tracked files, to refuse hidden source cards and redirects. It requires an existing Git workspace; if absent, request authorised Git setup before retrying. New or empty intake directories and shared `inbox/` alone do not choose a profile. Explicit selection is required. No tool silently renames paths, @@ -93,7 +93,7 @@ Project repositories retain field evidence such as meeting notes, interview tran The `knowledge/` directory holds curated findings and other knowledge objects in OKF Markdown, with source citations and timestamps. Each object captures a distinct finding or concept. A citation can point directly to field evidence or to a `source-document` record that identifies it. -In the single-repo Knowledge Store profile, the full uploaded file is retained in `_intake/originals/`. Its `_intake/processed/` record contains a summary and a reference to the original. Full document text belongs with the source evidence or the retained original. +A card in `sources/` contains a summary and a `source:` path or URI. To locate the original, follow that reference. In the single-repo Knowledge Store profile, an uploaded file is retained in `_intake/originals/` on the ingesting workspace. A card may instead cite a document already held at a shared location. OKF frontmatter per knowledge object (Google OKF v0.1 fields + Transitrix extensions): @@ -130,7 +130,7 @@ Quality gates are the rules the methodology places at the boundaries between lay ### Gate 1 — Quarantine on arrival -Every document admitted to the knowledge store enters as a `source-document` record in `_intake/processed/` (or its equivalent in a multi-repo deployment). No content is cited, answered from, or promoted until it has passed through the refinement layer. +Every document admitted to the knowledge store enters as a `source-document` record in `sources/` (or its equivalent in a multi-repo deployment). No content is cited, answered from, or promoted until it has passed through the refinement layer. **Rule:** raw material entering the refinement layer MUST be assigned the OKF `type: source-document` status on admission. Agents operating on the knowledge store MUST NOT read from `_intake/` as a source of answers; they read only from `knowledge/` (knowledge objects) and `canon/` (promoted primitives). The quarantine is not optional. @@ -192,7 +192,7 @@ Implementations SHOULD compute dependent counts from the canon graph before emit Every object in the knowledge store — source-document records and knowledge objects alike — MUST carry provenance (`source:`) and epistemic confidence (`confidence:`). Missing provenance blocks admission; missing confidence blocks promotion. **Rules:** -- `source:` (repo path or URI to the originating document) MUST be set before an object is admitted to `_intake/processed/`. +- `source:` (repo path or URI to the originating document) MUST be set before a source card is admitted to `sources/`. - `confidence:` MUST be set (`observed` / `inferred` / `assumed`) before a knowledge object is promoted to `knowledge/`. Default is forbidden — a curator must make an explicit assessment. - `timestamp:` MUST be updated when an object is modified. - **Low confidence + high dependents = review flag.** An object with `confidence: assumed` that has 3 or more downstream dependents MUST NOT be promoted without a review note explaining the assumption. Flag it in `_intake/log.md` and hold for sign-off. @@ -209,7 +209,7 @@ The first-build gates (1–4) catch shape, provenance, and duplicate *candidates |---|---| | **Mapping vocabulary** | Optional `mapping:` on a knowledge object MUST be one of `confirms` / `extends` / `proposes` / `conflicts` (Gate 2 vocabulary). | | **Declared conflicts** | `mapping: conflicts` MUST name the contradicted target in `conflicts_with:` (a `/knowledge/…` path or typed canon id). The object MUST NOT be treated as promoted until a human records the hold in `_intake/log.md`. | -| **Confidence ordering** | A knowledge object's `confidence:` MUST NOT rank higher than the `source-document` it cites when that source resolves to an `_intake/processed/` record (`assumed` < `inferred` < `observed`). | +| **Confidence ordering** | A knowledge object's `confidence:` MUST NOT rank higher than the `source-document` it cites when the citation resolves to a source record (`assumed` < `inferred` < `observed`). | **Out of scope for the open tier:** semantic contradiction detection (natural-language "these two paragraphs disagree"), cross-store reasoning, or canon-graph traversal beyond the explicit `conflicts_with:` pointer. Those belong to the proprietary advanced reasoner in DSM (see boundary below) — the open tier stays useful standalone without it. @@ -232,7 +232,7 @@ Ingestion is **assisted**, not autonomous: a proposer (model or agent) drafts ca 3. **Review** — curator approves, rejects, or resolves ambiguous items. Agents MUST NOT write to `knowledge/` before explicit approval. 4. **Dispose** — approved drafts are copied to `knowledge/` with draft-only fields (`review_status`, `ambiguity_note`) removed; the draft file is deleted; the linter runs again; `[admit]` is logged. Ambiguous items held for review are logged as `[ambig]` until resolved. -**Rule:** agents operating on the knowledge store MUST NOT read from `_intake/drafts/` as a source of answers — same quarantine discipline as `_intake/processed/` (Gate 1). Only `knowledge/` and `canon/` are answer surfaces. +**Rule:** agents operating on the knowledge store MUST NOT read from `_intake/drafts/` as a source of answers — same quarantine discipline as `sources/` (Gate 1). Only `knowledge/` and `canon/` are answer surfaces. **Rationale:** the proposer is fallible; the verifier is not. Separating draft from promoted zones localises model error to a disposable staging area and keeps promotion behind a human gate. @@ -249,7 +249,7 @@ What a conformant implementation MAY vary: What it MAY NOT vary: the admission rules, the risk-tier table, the mandatory-field requirements, the Gate 5 open-tier consistency rules, and the Gate 6 rule that proposers MUST NOT write directly to `knowledge/`. -**Reference implementation:** [`tools/knowledge_store_lint.py`](../tools/knowledge_store_lint.py) is a proof-point enforcement of Gates 1–6 over `_intake/processed/`, `_intake/drafts/`, and `knowledge/` — structural validation (required fields, the `confidence` enum), referential integrity (dangling bundle-relative links), duplicate-title detection (Gate 2), blast-radius tiering with the assumed-confidence review flag (Gate 3–4), scoped consistency checks (Gate 5 open tier), and draft-zone assisted-ingest rules (Gate 6). It is one conformant implementation, not the only one; a different tool MAY use a different shape/dedup engine as long as it satisfies the same rules. Run it with `python3 tools/knowledge_store_lint.py `. +**Reference implementation:** [`tools/knowledge_store_lint.py`](../tools/knowledge_store_lint.py) is a proof-point enforcement of Gates 1–6 over `sources/`, legacy `_intake/processed/`, `_intake/drafts/`, and `knowledge/` — structural validation (required fields, the `confidence` enum), referential integrity (dangling bundle-relative links), duplicate-title detection (Gate 2), blast-radius tiering with the assumed-confidence review flag (Gate 3–4), scoped consistency checks (Gate 5 open tier), and draft-zone assisted-ingest rules (Gate 6). It is one conformant implementation, not the only one; a different tool MAY use a different shape/dedup engine as long as it satisfies the same rules. Run it with `python3 tools/knowledge_store_lint.py `. **Validation codes (reference implementation):** @@ -257,11 +257,11 @@ What it MAY NOT vary: the admission rules, the risk-tier table, the mandatory-fi |---|---|---|---| | `KS-001` | error | — | Frontmatter block missing or not valid YAML. | | `KS-002` | error | — | Missing required field `type:`. | -| `KS-003` | error | 1 | `_intake/processed/` record does not carry `type: source-document` (quarantine violation). | +| `KS-003` | error | 1 | Source card does not carry `type: source-document` (quarantine violation). | | `KS-004` | error | 4 | Missing `source:` on an intake record — blocks admission. | | `KS-005` | error | 4 | Missing or invalid `confidence:` on a knowledge object — blocks promotion. | | `KS-006` | warning | 4 | Missing `timestamp:` on a knowledge object (recommended). | -| `KS-007` | error | 6 | Dangling bundle-relative link (`/knowledge/…`) in object body. | +| `KS-007` | error | 6 | Dangling bundle-relative link, unresolved source card citation, or invalid legacy source redirect. | | `KS-008` | warning | 2 | Title similarity suggests a possible duplicate — confirm Confirms/Extends, not silent mint. | | `KS-009` | info | 3 | Blast-radius tier (Low / Medium / High) from dependent-object count. | | `KS-010` | warning | 4 | `confidence: assumed` with 3+ dependents — requires review note in `_intake/log.md`. | @@ -288,10 +288,11 @@ For early adoption where standing up a separate knowledge repo adds too much ove _intake/ inbox/ ← incoming documents (gitignored — temporary staging) originals/ ← source files archived by reference (gitignored) - processed/ ← OKF source-document records (type: source-document) drafts/ ← proposer output (Gate 6 — not cited; not promoted until curator approves) log.md ← all events: [route] / [admit] / [ambig] / [conflicts] / [assert] +sources/ ← committed OKF source-document records + knowledge/ ← OKF knowledge objects (type: insight / finding / concept / ...) index.md ← auto-generated bundle index .md ← individual knowledge objects @@ -303,10 +304,27 @@ canon/ ← Transitrix primitives (unchanged) - **OKF track** — extract knowledge object drafts → verify with linter → curator reviews → promote to `knowledge/` → update `index.md` - **Canon track** — extract Transitrix primitives → open PR to `canon/` → merge gate -All incoming documents are always archived to `processed/` as OKF source-document records before routing begins. `originals/` is gitignored; the processed record carries `source:` (path or URI) and `source_hash:` (SHA-256) for integrity. +Record each incoming document as an OKF source card in `sources/` before routing. The card carries `source:` (path or URI) and `source_hash:` (SHA-256) for integrity. Keep the cards in Git. Migrate to a separate knowledge repo when the single-repo structure becomes congested or when a dedicated curation role is established. +## Move existing source cards + +Existing Knowledge Stores can adopt `sources/` for new cards while continuing to read cards in `_intake/processed/`. To move an existing card, keep its filename and any subdirectory. Check that the destination is free and both paths can be committed. Move only `type: source-document` records and leave each original where it is. Preserve the card's provenance, hash, and links to its evidence. If the card contains links relative to its old directory, adjust those links to keep their destinations. + +In the same commit, replace the old card with a Markdown redirect. For a card that moved from `_intake/processed/interview.md` to `sources/interview.md`, the old path contains: + +```markdown +--- +type: source-redirect +redirect_to: /sources/interview.md +--- + +[Open the source card](/sources/interview.md) +``` + +Knowledge Store resolves an old citation through the redirect. A reader opening the old file in GitHub can follow its link. Existing knowledge records and citations can remain as written. Run the Knowledge Store linter and the intake profile check before committing the move. Keep the redirects for as long as old citations are in use. + ## When to use - Multiple project repos producing source material that exceeds one team's capacity to review directly. @@ -327,7 +345,7 @@ Migrate to a separate knowledge repo when the single-repo structure becomes cong Starter templates for the two core OKF record types live alongside this pattern: -- [`knowledge-store-templates/okf-source-document.md`](knowledge-store-templates/okf-source-document.md) — copy to `_intake/processed/` for each ingested document; covers `type`, `source`, `source_hash`, `confidence`, `tracks`, and routing notes +- [`knowledge-store-templates/okf-source-document.md`](knowledge-store-templates/okf-source-document.md) — copy to `sources/` for each ingested document; covers `type`, `source`, `source_hash`, `confidence`, `tracks`, and routing notes - [`knowledge-store-templates/okf-knowledge-object.md`](knowledge-store-templates/okf-knowledge-object.md) — copy to `knowledge/` for each extracted concept; covers `type`, `description`, `resource`, `confidence`, citations, and examples - [`knowledge-store-templates/okf-knowledge-object-draft.md`](knowledge-store-templates/okf-knowledge-object-draft.md) — copy to `_intake/drafts/` during assisted ingest (Gate 6); adds `review_status` and `ambiguity_note` @@ -363,7 +381,7 @@ type: concept title: Multi-tenant isolation description: A security boundary between customer datasets; enforced at the row level via application logic. confidence: inferred -source: /_intake/processed/2025-03-15-architecture-audit.md +source: /sources/2025-03-15-architecture-audit.md mapping: confirms created_at: 2025-03-15 timestamp: 2025-03-15T10:30:00Z @@ -386,7 +404,7 @@ type: concept title: Multi-tenant isolation (updated 2026) description: A security boundary between customer datasets; enforced via a combination of row-level application logic, database-level schema separation, and cryptographic key isolation depending on trust tier. confidence: observed -source: /_intake/processed/2026-09-02-multi-tenancy-security-audit.md +source: /sources/2026-09-02-multi-tenancy-security-audit.md mapping: extends created_at: 2026-09-02 supersedes: /knowledge/multi-tenant-isolation.md diff --git a/publish-manifest.json b/publish-manifest.json index f7ac6f9c..281b0227 100644 --- a/publish-manifest.json +++ b/publish-manifest.json @@ -770,6 +770,7 @@ "transitrix/skills/knowledge-store/tests/fixtures/valid/_intake/processed/2026-07-01-cfo-interview.md", "transitrix/skills/knowledge-store/tests/fixtures/valid/knowledge/order-fulfilment-bottleneck.md", "transitrix/skills/knowledge-store/tests/fixtures/valid/knowledge/warehouse-finance-handoff.md", + "transitrix/skills/knowledge-store/tests/fixtures/valid/sources/2026-07-01-cfo-interview.md", "transitrix/skills/knowledge-store/tests/test_knowledge_store_integrity.py", "transitrix/skills/onboard/README.md", "transitrix/skills/onboard/SKILL.md", diff --git a/scripts/check-notations.mjs b/scripts/check-notations.mjs index b2fea7a9..36bb47f4 100644 --- a/scripts/check-notations.mjs +++ b/scripts/check-notations.mjs @@ -103,7 +103,7 @@ import { readFile, readdir, stat } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import { dirname, join, relative, resolve, posix } from 'node:path'; +import { dirname, join, relative, resolve, posix, sep } from 'node:path'; const __filename = fileURLToPath(import.meta.url); const REPO_ROOT = join(dirname(__filename), '..'); @@ -375,14 +375,14 @@ export function resolveInstructionLink(abs, target, root = REPO_ROOT) { } return null; // not an installed guide, even if a same-named source file exists } - // Knowledge fixtures use site-root /knowledge links. Bind to the fixture's - // own root, never the machine root or a different fixture's knowledge tree. + // Knowledge fixtures use bundle-root links. Bind them to the fixture's + // own root to preserve bundle links across repositories and platforms. const fixture = rel.match(/^(transitrix\/skills\/knowledge-store\/tests\/fixtures\/[^/]+)\//); if (fixture && target.startsWith('/')) { if (target.split('/').includes('..')) return null; const fixtureRoot = resolve(root, fixture[1]); const resolved = resolve(fixtureRoot, '.' + target); - return resolved.startsWith(fixtureRoot + '/') ? resolved : null; + return resolved.startsWith(fixtureRoot + sep) ? resolved : null; } return resolve(dirname(abs), target); } diff --git a/tools/knowledge_store_lint.py b/tools/knowledge_store_lint.py index 4d4218d5..33a7e758 100644 --- a/tools/knowledge_store_lint.py +++ b/tools/knowledge_store_lint.py @@ -5,8 +5,8 @@ Companion to tools/lint.py (which validates canon/ YAML elements). This script validates the knowledge store's refinement layer instead: -_intake/processed/ (OKF source-document records), _intake/drafts/ (proposer -drafts), and knowledge/ (OKF knowledge objects), per the Quality gates in +sources/ (OKF source-document records), legacy _intake/processed/ records, +_intake/drafts/ (proposer drafts), and knowledge/ (OKF knowledge objects), per the Quality gates in patterns/knowledge-store.md. This is the "proof point" reference implementation for the six quality @@ -30,6 +30,9 @@ FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n?(.*)$", re.DOTALL) BUNDLE_LINK_RE = re.compile(r"\]\((/knowledge/[^)\s]+\.md)\)") +SOURCE_LINK_RE = re.compile(r"\]\((/(?:sources|_intake/processed|intake/processed)/[^)\s#]+\.md)(?:#[^)\s]+)?\)") +SOURCE_PREFIX = "sources/" +LEGACY_SOURCE_PREFIX = "_intake/processed/" CONFIDENCE_ENUM = {"observed", "inferred", "assumed"} CONFIDENCE_RANK = {"assumed": 0, "inferred": 1, "observed": 2} MAPPING_ENUM = {"confirms", "extends", "proposes", "conflicts"} @@ -61,12 +64,14 @@ class KnowledgeObject: class KnowledgeStoreLinter: def __init__(self, root: str = "."): self.root = Path(root) + self.source_dir = self.root / "sources" self.intake_dir = self.root / "_intake" / "processed" self.drafts_dir = self.root / "_intake" / "drafts" self.knowledge_dir = self.root / "knowledge" self.log_path = self.root / "_intake" / "log.md" self.findings: List[Finding] = [] self.objects: List[KnowledgeObject] = [] + self.source_records: Dict[str, KnowledgeObject] = {} self.freshness_thresholds = self._load_freshness_thresholds() def _load_freshness_thresholds(self) -> Dict[str, int]: @@ -90,10 +95,11 @@ def run(self) -> bool: print() self._load_objects() - if not self.objects: - print("No knowledge-store objects found under _intake/processed/, _intake/drafts/, or knowledge/ — nothing to check.") + if not self.objects and not self.findings: + print("No knowledge-store objects found under sources/, _intake/processed/, _intake/drafts/, or knowledge/ — nothing to check.") return True + self._index_sources() self._check_structural() self._check_referential_integrity() self._check_duplicates() @@ -111,6 +117,7 @@ def run(self) -> bool: def _load_objects(self): for zone, directory in ( + ("intake", self.source_dir), ("intake", self.intake_dir), ("draft", self.drafts_dir), ("knowledge", self.knowledge_dir), @@ -141,6 +148,37 @@ def _load_objects(self): path=path, rel_path=rel, frontmatter=fm, body=m.group(2), zone=zone, )) + @staticmethod + def _is_source_redirect(obj: KnowledgeObject) -> bool: + return ( + obj.rel_path.startswith(LEGACY_SOURCE_PREFIX) + and obj.frontmatter.get("type") == "source-redirect" + ) + + def _index_sources(self): + self.source_records = { + obj.rel_path: obj for obj in self.objects + if obj.zone == "intake" and obj.frontmatter.get("type") == "source-document" + } + for obj in self.objects: + if not self._is_source_redirect(obj): + continue + # A move preserves the filename and relative subdirectory. Require a + # real source record and a visible Markdown link for repository readers. + target = SOURCE_PREFIX + obj.rel_path[len(LEGACY_SOURCE_PREFIX):] + if ( + obj.frontmatter.get("redirect_to") != "/" + target + or "/" + target not in SOURCE_LINK_RE.findall(obj.body) + or target not in self.source_records + ): + self.findings.append(Finding( + obj.rel_path, "KS-007", "error", + f"Source redirect must cite the existing source card `/{target}` " + "in both `redirect_to:` and a Markdown link.", + )) + continue + self.source_records[obj.rel_path] = self.source_records[target] + # --- structural (Gate 1, Gate 4, item 5) ---------------------------- def _check_structural(self): @@ -153,16 +191,16 @@ def _check_structural(self): "Missing required field `type:`.", )) - if obj.zone == "intake": + if obj.zone == "intake" and not self._is_source_redirect(obj): if fm.get("type") != "source-document": self.findings.append(Finding( obj.rel_path, "KS-003", "error", - f"_intake/processed/ record must carry `type: source-document` (Gate 1 quarantine); found `{fm.get('type')!r}`.", + f"Source record must carry `type: source-document` (Gate 1 quarantine); found `{fm.get('type')!r}`.", )) if not fm.get("source"): self.findings.append(Finding( obj.rel_path, "KS-004", "error", - "Missing `source:` — required before admission to _intake/processed/ (Gate 4).", + "Missing `source:` — required for source records (Gate 4).", )) if obj.zone in ("knowledge", "draft"): @@ -204,6 +242,17 @@ def _check_referential_integrity(self): f"Dangling link to `{target}` — no such file under knowledge/.", )) + source_links = SOURCE_LINK_RE.findall(obj.body) + source = self._norm_source_path(obj.frontmatter.get("source") or "") + if obj.zone in ("knowledge", "draft") and source.startswith((SOURCE_PREFIX, LEGACY_SOURCE_PREFIX)): + source_links.append(source) + for target in sorted({self._norm_source_path(link) for link in source_links}): + if target not in self.source_records: + self.findings.append(Finding( + obj.rel_path, "KS-007", "error", + f"Dangling source reference `/{target}`.", + )) + # --- duplicate detection (item 6b, Gate 2) -------------------------- def _check_duplicates(self): @@ -291,28 +340,32 @@ def _check_blast_radius_and_confidence(self): def _norm_source_path(self, source: str) -> str: s = str(source).strip().replace("\\", "/") - if s.startswith("/"): - s = s.lstrip("/") + s = s.lstrip("/") + # Early templates emitted this spelling without the underscore. + if s.startswith("intake/processed/"): + s = "_" + s + if s.startswith((SOURCE_PREFIX, LEGACY_SOURCE_PREFIX)): + s = s.split("#", 1)[0] return s - def _intake_by_rel(self) -> Dict[str, KnowledgeObject]: - return {o.rel_path: o for o in self.objects if o.zone == "intake"} - def _knowledge_by_rel(self) -> Dict[str, KnowledgeObject]: return {o.rel_path: o for o in self.objects if o.zone == "knowledge"} - def _resolve_intake_source(self, source: Optional[str]) -> Optional[KnowledgeObject]: + def _resolve_source_card(self, source: Optional[str]) -> Optional[KnowledgeObject]: if not source: return None norm = self._norm_source_path(source) - intake = self._intake_by_rel() - if norm in intake: - return intake[norm] - # Also match when source omits leading path segments. - for rel, obj in intake.items(): - if rel.endswith("/" + norm) or rel == norm: - return obj - return None + if norm in self.source_records: + return self.source_records[norm] + if norm.startswith((SOURCE_PREFIX, LEGACY_SOURCE_PREFIX)): + return None + # Older records may omit leading directories. Match only when the + # suffix identifies one card, including aliases for that same card. + matches = { + obj.rel_path: obj for rel, obj in self.source_records.items() + if rel.endswith("/" + norm) + } + return next(iter(matches.values())) if len(matches) == 1 else None def _conflicts_with_resolves(self, target: str, knowledge: Dict[str, KnowledgeObject]) -> bool: if not target: @@ -372,7 +425,7 @@ def _check_consistency(self): "_intake/log.md before promotion (Gate 5).", )) - src = self._resolve_intake_source(fm.get("source")) + src = self._resolve_source_card(fm.get("source")) if src: obj_conf = fm.get("confidence") src_conf = src.frontmatter.get("confidence") diff --git a/tools/tests/test_knowledge_supersession.py b/tools/tests/test_knowledge_supersession.py index 9f223b22..37ee0231 100644 --- a/tools/tests/test_knowledge_supersession.py +++ b/tools/tests/test_knowledge_supersession.py @@ -442,7 +442,11 @@ def test_template_defaults(): timestamp="2026-09-13T00:00:00Z") if zone == "_intake/drafts": frontmatter["review_status"] = "ready" + check(frontmatter["source"].startswith("/sources/"), + f"{name}: new citations must point to sources/") with tempfile.TemporaryDirectory(prefix="knowledge-template-") as work: + _write(os.path.join(work, frontmatter["source"].lstrip("/")), + "---\ntype: source-document\nsource: original.md\nconfidence: observed\n---\n") _write(os.path.join(work, zone, "first.md"), "---\n" + yaml.safe_dump(frontmatter) + "---\nFirst assertion.\n") code, out = _run_linter(work) diff --git a/transitrix/skills/knowledge-store/README.md b/transitrix/skills/knowledge-store/README.md index d3c50984..5e8c4ca1 100644 --- a/transitrix/skills/knowledge-store/README.md +++ b/transitrix/skills/knowledge-store/README.md @@ -4,6 +4,10 @@ The **OKF ingestion skill** for a Transitrix knowledge store. Processes raw sour This directory is the **`knowledge-store` skill** within the `transitrix` plugin. Invoked as `/transitrix:knowledge-store`. +## Source cards and original files + +New cards go in `sources/`. Open the card's `source:` reference to locate the original. Uploaded files in the single-repo profile are kept in the ingesting workspace's `_intake/originals/`. Legacy cards remain readable and can be moved with [a redirect at the old path](../../../patterns/knowledge-store.md#move-existing-source-cards). + ## Intake profile selection Choose `ingest` for the ordinary ingest/reg-intel workflow (private processed raw diff --git a/transitrix/skills/knowledge-store/SKILL.md b/transitrix/skills/knowledge-store/SKILL.md index 56750a41..ef5e771e 100644 --- a/transitrix/skills/knowledge-store/SKILL.md +++ b/transitrix/skills/knowledge-store/SKILL.md @@ -1,7 +1,7 @@ --- name: Transitrix Knowledge Store description: Ingest raw source material into the OKF single-repo MVP knowledge store — assess, archive, route to the OKF track (extract drafts → verify → human review → promote to knowledge/) and/or the Canon track (extract Transitrix primitives → PR to canon/), and log every decision. -when_to_use: User says "ingest this document", "add this to the knowledge store", "extract knowledge objects from this", "route this to canon", or drops a file into `_intake/inbox/` and wants it processed. +when_to_use: User says "ingest this document", "add this to the knowledge store", "extract knowledge objects from this", "route this to canon", "locate the original for this knowledge item", or drops a file into `_intake/inbox/` and wants it processed. min_version: "6.0.0" allowed-tools: Read, Write, Edit, Bash, Glob, Grep --- @@ -26,6 +26,10 @@ The linter enforces Gates 1–6 from [patterns/knowledge-store.md §Quality gate --- +## Locate an original + +Start with the knowledge object's `source:` citation or the source card the user names. Follow a legacy redirect if present, then read the card's `source:` path or URI. For an uploaded original, a path under `_intake/originals/` refers to the workspace that retained it. Report the location and whether you can access it from the current workspace. A source-location request is read-only and can skip intake setup. + ## Step 0 — Check the MVP structure Before any write, select the **knowledge-store** deployment profile using the @@ -48,16 +52,17 @@ Verify the repo has the expected layout: _intake/ inbox/ ← source files waiting for processing originals/ ← raw source files (gitignored) - processed/ ← OKF source-document records (committed) drafts/ ← proposer output (Gate 6 — not cited; not promoted) log.md ← all events: [route] / [admit] / [ambig] / [conflicts] / [assert] +sources/ ← OKF source-document records (committed) + knowledge/ index.md ← bundle index (auto-maintained) .md ← individual OKF knowledge objects ``` -If `_intake/` or `knowledge/` are missing, scaffold them now. Create `_intake/drafts/` if absent. Copy the initialisation content from [patterns/knowledge-store.md §Templates](../../../patterns/knowledge-store.md). Add gitignore entries for `_intake/inbox/*` and `_intake/originals/*` (drafts MAY be committed for audit or gitignored for ephemeral runs). Never ignore `_intake/processed/`. Re-run the profile check after setup and before committing; explicitly verify each new source-record path is not ignored before writing it. +Scaffold any missing `_intake/`, `sources/`, and `knowledge/` directories. Create `_intake/drafts/` if absent. Copy the initialisation content from [patterns/knowledge-store.md §Templates](../../../patterns/knowledge-store.md). Add gitignore entries for `_intake/inbox/*` and `_intake/originals/*` (drafts MAY be committed for audit or gitignored for ephemeral runs). Keep `sources/` and any existing legacy cards or redirects visible to Git. See [moving existing cards](../../../patterns/knowledge-store.md#move-existing-source-cards). Re-run the profile check after setup and before committing; explicitly verify each new source-record path is not ignored before writing it. --- @@ -78,14 +83,14 @@ State your assessment to the user before proceeding. --- -## Step 2 — Archive to processed/ +## Step 2 — Record the source in sources/ -Copy the source file reference to `_intake/processed/` as an OKF source-document record using the template at [patterns/knowledge-store-templates/okf-source-document.md](../../../patterns/knowledge-store-templates/okf-source-document.md). +Record the source file reference in `sources/` as an OKF source-document record using the template at [patterns/knowledge-store-templates/okf-source-document.md](../../../patterns/knowledge-store-templates/okf-source-document.md). Fill in: - `title:` — document filename or stated title - `description:` — one-sentence summary from Step 1 -- `source:` — original file path or URI (e.g. `_intake/inbox/filename.md` or a web URL) +- `source:` — retained original path or URI, such as `_intake/originals/filename.md` for an uploaded file or a stable URI for an existing shared source - `source_hash:` — SHA-256 of the source file (`sha256sum` / `Get-FileHash`) - `created_at:` — today's date - `confidence:` — from Step 1 @@ -94,9 +99,9 @@ Fill in: Filename convention: `YYYY-MM-DD-.md` where slug is a 3–5 word kebab-case summary. -For a reissued source, use a fresh processed-record and original-file path even when the date or filename matches an earlier revision. Preserve the earlier record, original, and hash. +For a reissued source, use a fresh card and original-file path even when the date or filename matches an earlier revision. Preserve the earlier record, original, and hash. -Move the source file from `_intake/inbox/` to `_intake/originals/` (gitignored, kept for traceability). +Move an uploaded source file from `_intake/inbox/` to the final `_intake/originals/` path (gitignored, kept for traceability). Use that final path in the card. --- @@ -107,7 +112,7 @@ Append to `_intake/log.md`: ```markdown ## YYYY-MM-DD -- [route] `` → tracks: [okf, canon] | confidence: observed | +- [route] `` → tracks: [okf, canon] | confidence: observed | ``` --- @@ -116,7 +121,7 @@ Append to `_intake/log.md`: ### 4a — Propose drafts (Gate 6) -Read the source document from `_intake/originals/` (or the processed record). Using the extraction prompt at [prompts/extract-okf.md](prompts/extract-okf.md), extract candidate knowledge objects and **write each to `_intake/drafts/.md`** using the template at [patterns/knowledge-store-templates/okf-knowledge-object-draft.md](../../../patterns/knowledge-store-templates/okf-knowledge-object-draft.md). +Read the source document at the location recorded in the card. Using the extraction prompt at [prompts/extract-okf.md](prompts/extract-okf.md), extract candidate knowledge objects and **write each to `_intake/drafts/.md`** using the template at [patterns/knowledge-store-templates/okf-knowledge-object-draft.md](../../../patterns/knowledge-store-templates/okf-knowledge-object-draft.md). Each draft: - has a clear, discrete idea (one concept per file) @@ -189,7 +194,7 @@ type: DRIVER id: -TBD # review placeholder, never an admitted ID name: "" description: "" -derived_from: [] +derived_from: [] admitted_to: pending extraction_confidence: high | medium | low ``` @@ -215,7 +220,7 @@ extraction_confidence: high | medium | low Create a branch `knowledge-store/ingest--` and commit only the reviewed, validated candidate files for human admission. Open a PR titled `[Knowledge Store] Ingest: `. PR body must include: -- Link to the processed source-document record +- Link to the source card - Table of proposed elements with type, name, and extraction_confidence - Note on any relations proposed - Instruction: "Review the assigned IDs, resolved references, provenance and validation results; complete admission only when satisfied. Never merge unresolved placeholders or pending proposals as admitted canon." @@ -225,7 +230,7 @@ PR body must include: Once the user reports the PR outcome, append to `_intake/log.md`: ```markdown -- [assert] PR # merged | admitted: [, , ...] | source: +- [assert] PR # merged | admitted: [, , ...] | source: ``` Or for rejected: diff --git a/transitrix/skills/knowledge-store/prompts/extract-canon.md b/transitrix/skills/knowledge-store/prompts/extract-canon.md index 1944bc81..0a5c948c 100644 --- a/transitrix/skills/knowledge-store/prompts/extract-canon.md +++ b/transitrix/skills/knowledge-store/prompts/extract-canon.md @@ -33,7 +33,7 @@ type: DRIVER id: -TBD # leave TBD — IDs are assigned during review to avoid conflicts name: "" description: "" -derived_from: [/_intake/processed/] +derived_from: [/sources/] admitted_to: pending extraction_confidence: high | medium | low ``` diff --git a/transitrix/skills/knowledge-store/prompts/extract-okf.md b/transitrix/skills/knowledge-store/prompts/extract-okf.md index e957ab23..d0ea77cb 100644 --- a/transitrix/skills/knowledge-store/prompts/extract-okf.md +++ b/transitrix/skills/knowledge-store/prompts/extract-okf.md @@ -26,7 +26,7 @@ Write each candidate to `_intake/drafts/.md` (do not write to `knowledge/` type: insight | finding | concept | question title: "" description: "" -source: /_intake/processed/ +source: /sources/ created_at: YYYY-MM-DD confidence: observed | inferred | assumed mapping: confirms | extends | proposes | conflicts # optional @@ -40,7 +40,7 @@ timestamp: YYYY-MM-DDTHH:MM:SSZ Citations: -- [](/_intake/processed/) +- [](/sources/) ~~~ ## Confidence assignment diff --git a/transitrix/skills/knowledge-store/tests/fixtures/valid/_intake/processed/2026-07-01-cfo-interview.md b/transitrix/skills/knowledge-store/tests/fixtures/valid/_intake/processed/2026-07-01-cfo-interview.md index 6c483031..39c172c7 100644 --- a/transitrix/skills/knowledge-store/tests/fixtures/valid/_intake/processed/2026-07-01-cfo-interview.md +++ b/transitrix/skills/knowledge-store/tests/fixtures/valid/_intake/processed/2026-07-01-cfo-interview.md @@ -1,16 +1,6 @@ --- -type: source-document -title: "CFO onboarding interview" -description: "First-hand account of the order-fulfilment pain points from the CFO." -source: "_intake/originals/2026-07-01-cfo-interview.md" -source_hash: "sha256:0000000000000000000000000000000000000000000000000000000000000001" -created_at: 2026-07-01 -confidence: observed -tags: [interview, fulfilment] -tracks: [okf] -status: processed +type: source-redirect +redirect_to: /sources/2026-07-01-cfo-interview.md --- -# Summary - -The CFO described manual hand-offs between warehouse and finance as the primary bottleneck. +[Open the source card](/sources/2026-07-01-cfo-interview.md) diff --git a/transitrix/skills/knowledge-store/tests/fixtures/valid/knowledge/order-fulfilment-bottleneck.md b/transitrix/skills/knowledge-store/tests/fixtures/valid/knowledge/order-fulfilment-bottleneck.md index 7d8fd405..61033078 100644 --- a/transitrix/skills/knowledge-store/tests/fixtures/valid/knowledge/order-fulfilment-bottleneck.md +++ b/transitrix/skills/knowledge-store/tests/fixtures/valid/knowledge/order-fulfilment-bottleneck.md @@ -15,4 +15,4 @@ Warehouse picks are complete before finance receives the invoice trigger, creati # Citations -- CFO onboarding interview (`_intake/processed/2026-07-01-cfo-interview.md`) +- [CFO onboarding interview](/_intake/processed/2026-07-01-cfo-interview.md) diff --git a/transitrix/skills/knowledge-store/tests/fixtures/valid/knowledge/warehouse-finance-handoff.md b/transitrix/skills/knowledge-store/tests/fixtures/valid/knowledge/warehouse-finance-handoff.md index 34667488..24954f9e 100644 --- a/transitrix/skills/knowledge-store/tests/fixtures/valid/knowledge/warehouse-finance-handoff.md +++ b/transitrix/skills/knowledge-store/tests/fixtures/valid/knowledge/warehouse-finance-handoff.md @@ -2,7 +2,7 @@ type: finding title: "Warehouse-finance handoff delay" description: "Finance invoice trigger arrives days after warehouse completion." -source: _intake/processed/2026-07-01-cfo-interview.md +source: /sources/2026-07-01-cfo-interview.md created_at: 2026-07-01 confidence: observed tags: [finance, warehouse] diff --git a/transitrix/skills/knowledge-store/tests/fixtures/valid/sources/2026-07-01-cfo-interview.md b/transitrix/skills/knowledge-store/tests/fixtures/valid/sources/2026-07-01-cfo-interview.md new file mode 100644 index 00000000..6c483031 --- /dev/null +++ b/transitrix/skills/knowledge-store/tests/fixtures/valid/sources/2026-07-01-cfo-interview.md @@ -0,0 +1,16 @@ +--- +type: source-document +title: "CFO onboarding interview" +description: "First-hand account of the order-fulfilment pain points from the CFO." +source: "_intake/originals/2026-07-01-cfo-interview.md" +source_hash: "sha256:0000000000000000000000000000000000000000000000000000000000000001" +created_at: 2026-07-01 +confidence: observed +tags: [interview, fulfilment] +tracks: [okf] +status: processed +--- + +# Summary + +The CFO described manual hand-offs between warehouse and finance as the primary bottleneck. diff --git a/transitrix/skills/knowledge-store/tests/test_knowledge_store_integrity.py b/transitrix/skills/knowledge-store/tests/test_knowledge_store_integrity.py index 41df7c35..db1c0013 100644 --- a/transitrix/skills/knowledge-store/tests/test_knowledge_store_integrity.py +++ b/transitrix/skills/knowledge-store/tests/test_knowledge_store_integrity.py @@ -69,6 +69,47 @@ def _with_copy(mutator): shutil.rmtree(tmp, ignore_errors=True) +def part_source_card_compatibility(): + print("Source cards and legacy citations") + card = "2026-07-01-cfo-interview.md" + + def lower_confidence(root): + path = os.path.join(root, "sources", card) + with open(path, encoding="utf-8") as handle: + content = handle.read().replace("confidence: observed", "confidence: assumed") + with open(path, "w", encoding="utf-8") as handle: + handle.write(content) + + code, out = _with_copy(lower_confidence) + check(code == 0 and out.count("[KS-013]") == 2, + f"Both old and new citations must use the moved card's confidence.\n{out}") + + def delete_redirect(root): + os.remove(os.path.join(root, "_intake", "processed", card)) + + code, out = _with_copy(delete_redirect) + check(code != 0 and "[KS-007]" in out and f"/_intake/processed/{card}" in out, + f"A missing redirect must not silently resolve to a new path.\n{out}") + + def delete_target(root): + os.remove(os.path.join(root, "sources", card)) + + code, out = _with_copy(delete_target) + check(code != 0 and "[KS-007]" in out and "Source redirect must cite" in out, + f"A redirect with a missing target must fail validation.\n{out}") + + def restore_legacy_card(root): + shutil.move(os.path.join(root, "sources", card), os.path.join(root, "_intake", "processed", card)) + path = os.path.join(root, "knowledge", "warehouse-finance-handoff.md") + with open(path, encoding="utf-8") as handle: + content = handle.read().replace("/sources/", "/_intake/processed/") + with open(path, "w", encoding="utf-8") as handle: + handle.write(content) + + code, out = _with_copy(restore_legacy_card) + check(code == 0, f"A store with only original legacy cards must still pass.\n{out}") + + def part_c_gate_violations(): print("Part C — gate violations (expected failures)") @@ -81,7 +122,7 @@ def missing_frontmatter(root): check(code != 0 and "KS-001" in out, f"Missing frontmatter should fail KS-001; code={code}\n{out}") def wrong_intake_type(root): - path = os.path.join(root, "_intake", "processed", "2026-07-01-cfo-interview.md") + path = os.path.join(root, "sources", "2026-07-01-cfo-interview.md") text = open(path, encoding="utf-8").read().replace("type: source-document", "type: insight") with open(path, "w", encoding="utf-8") as f: f.write(text) @@ -90,7 +131,7 @@ def wrong_intake_type(root): check(code != 0 and "KS-003" in out, f"Wrong intake type should fail KS-003; code={code}\n{out}") def missing_source(root): - path = os.path.join(root, "_intake", "processed", "2026-07-01-cfo-interview.md") + path = os.path.join(root, "sources", "2026-07-01-cfo-interview.md") text = open(path, encoding="utf-8").read().replace("source:", "source_removed:") with open(path, "w", encoding="utf-8") as f: f.write(text) @@ -213,7 +254,7 @@ def conflicts_no_log(root): check(code != 0 and "KS-014" in out, f"conflicts without log should fail KS-014; code={code}\n{out}") def confidence_ordering(root): - intake_path = os.path.join(root, "_intake", "processed", "2026-07-01-cfo-interview.md") + intake_path = os.path.join(root, "sources", "2026-07-01-cfo-interview.md") intake_text = open(intake_path, encoding="utf-8").read().replace( "confidence: observed", "confidence: inferred" ) @@ -301,6 +342,7 @@ def main(): sys.stdout.reconfigure(encoding="utf-8") part_a_bundle_integrity() part_b_valid_fixture() + part_source_card_compatibility() part_c_gate_violations() part_d_warnings() part_e_gate5_consistency()