From a073e55457569df897c55f922e5853d2bf66f3d7 Mon Sep 17 00:00:00 2001 From: Mohammad Wahbeh Date: Wed, 26 Aug 2026 01:28:42 +0300 Subject: [PATCH 1/2] feat(tooling): add a query CLI and lookup skill for docs/knowledge/ The corpus is 39 topic files and ~1470 entries. Answering "what do we already know about RETRY-13" meant grepping blind or reading a whole 20 KB file; there was no requirement-ID index and no query surface. scripts/knowledge.mjs parses the corpus into entry records and filters them by requirement ID, topic, section, provenance role, styleguide chapter, and text. Different filters AND together; values within one filter OR. A requirement-ID query runs ~120-580 tokens against a topic file of ~1800-5200. Two behaviours are load-bearing rather than incidental: - The ID prefix allowlist is derived from appendix C at runtime, never hardcoded. A bare \b[A-Z]{2,12}-\d+\b also claims UTF-8, SHA-256, ISO-8601 and RFC-3986; parsing failure throws rather than falling back to that regex. Matching tokenizes then compares whole tokens, so --req HTTP-7 cannot match HTTP-70. - Appendix B is the conformance checklist: its entries roll several IDs into one sentence and state none of them. 256 of the 641 cited IDs resolve only to a roll-up, which exits 0 and reads as answered. Those results are tagged [appendix-B roll-up], a --req answered entirely by them warns, and --coverage reports substantive (385) separately from roll-up-only (256) and uncited (4). The skill carries the workflow the CLI alone cannot: run --section conflicts once per phase (6 entries corpus-wide, where design-vs- styleguide contradictions are recorded resolved or open); pass a task's whole ID set in one call; check a hit is not a roll-up before trusting it; reach the 16 topics that carry no requirement ID via --topic or --chapter. It also records that styleguide paths are absolute to a sibling repo and need their machine prefix stripped before being used as a citation. Tests run under node --test, not bun test: bunfig.toml scopes discovery to packages so the 80% coverage floor stays a statement about packages/core rather than about repo tooling. No CI step. --coverage is a report run by hand, and --list-reqs prints to stdout rather than generating another doc. --- .claude/skills/knowledge-lookup/SKILL.md | 157 +++++ bunfig.toml | 4 + package.json | 2 + scripts/knowledge.mjs | 750 +++++++++++++++++++++++ scripts/knowledge.test.mjs | 375 ++++++++++++ 5 files changed, 1288 insertions(+) create mode 100644 .claude/skills/knowledge-lookup/SKILL.md create mode 100644 scripts/knowledge.mjs create mode 100644 scripts/knowledge.test.mjs diff --git a/.claude/skills/knowledge-lookup/SKILL.md b/.claude/skills/knowledge-lookup/SKILL.md new file mode 100644 index 0000000..642625e --- /dev/null +++ b/.claude/skills/knowledge-lookup/SKILL.md @@ -0,0 +1,157 @@ +--- +name: knowledge-lookup +description: Use when starting a numbered task from a docs/superpowers/plans/ file, implementing or reviewing against a requirement ID (HTTP-7, SEAM-1, RETRY-13, NFR-5), or resolving a styleguide citation such as "styleguide 6.7" or "ch08". +--- + +# Knowledge Lookup + +## Overview + +`docs/knowledge/` is 39 topic files and ~1470 harvested entries — 512 KB, past what belongs +in context. `bun run knowledge` filters it. A requirement-ID query runs ~120–580 tokens +(median ~230) against a topic file of ~1800–5200 (median ~2300): roughly 9× smaller, and +much more than that when the ID you want lives in a file you'd never have guessed. + +Every entry is one bullet plus a `` line carrying role, source path, line range, and +sha — the citation a test-file header or deferral note needs. + +## Start of a phase: run this once + +```bash +bun run knowledge --section conflicts --brief # 6 entries corpus-wide, ~1.1k tokens +``` + +Six entries exist. They are where a design-vs-styleguide contradiction is recorded as +resolved or still open, and a plan's Global Constraints may assert as settled something the +corpus still lists **unresolved**. Nothing else in this workflow will surface them. + +## Starting a numbered task: one query, not six + +Plan tasks list their requirement IDs in the task header. Pass the whole set at once — +`--req` accepts commas and ORs within itself: + +```bash +bun run knowledge --req HTTP-13,HTTP-14,HTTP-15,HTTP-16,HTTP-3,HTTP-5 +``` + +Different filters AND together; multiple values inside one filter OR. So +`--req A --req B --topic headers` means "(cites A or B) and is in a headers file". + +## Check the result is real before trusting it + +**A `--req` hit is not proof the corpus knows anything.** 256 of the 641 cited IDs resolve +*only* to an appendix-B conformance roll-up — one sentence naming three to five IDs and +stating none of them. It exits 0, so nothing else will warn you. + +The CLI tags these `[appendix-B roll-up]` and prints a WARNING when every hit is one. When +you see it, stop querying and go to the source: + +```bash +grep -n '^| HTTP-10 ' docs/product-spec/appendix-c-consolidated-normative-requirement-index.md +``` + +The leading `| ` and trailing space are load-bearing — `grep 'HTTP-1'` matches HTTP-10 +through HTTP-19. + +## Two entry points + +**ID-first — you have requirement IDs.** This is the plan-task case. + +1. `bun run knowledge --req ` — what the corpus concluded. Design-role entries quote + `docs/sdk-design-nodejs/` inline, so this usually covers the TypeScript mapping too; add + `--role design` to isolate them. Open the design doc only to follow a line range. +2. `grep -n '^| ' docs/product-spec/appendix-c-…md` — canonical text, when the query + came back a roll-up or you need the normative wording verbatim. + +**Topic-first — you have an area, or a styleguide citation.** + +```bash +bun run knowledge --list-topics # 39 topics, entry and ID counts +bun run knowledge --topic pipeline --section rules --brief cursor fork +``` + +**16 of the 39 topics carry no requirement ID at all** — every styleguide-derived one, +including `data-modeling`, `error-handling`, `assertions`, `testing`, `api-design`. ID-first +cannot reach them. `--list-topics` shows which; don't work from a memorised list. + +For "styleguide 6.7" / "ch08", use `--chapter`: + +```bash +bun run knowledge --chapter 6 interface class # styleguide 6.7 → the classes chapter +``` + +Entries record a chapter file and line range, never a section number, so `--chapter 6.7` +queries chapter 6 and tells you it dropped the `.7`. Narrow with bare words instead. + +## Never read a whole topic file — with two stated exceptions + +**Reading a topic file when a filtered query answers the question is the failure this tool +exists to prevent.** Not "I'll grep it myself" — `grep` has no section, role, or exact-token +ID matching. Not "I need surrounding context" — widen the filter first. + +The two cases where reading is correct, and how: + +- **An unnarrowed `--topic` costs more than the file.** `--topic http-domain-model` is + 22,196 bytes; the file is 20,157. A topic query without `--section`, `--chapter`, or bare + words is not a filter. Add one, or read the file — don't run the query. +- **Following up a located entry, when you need the exact bytes.** The bullet sits at the + printed line, its `` at line+1, entries are 2 lines with no blank between — so read + an even span starting on the bullet or you will split a rule from its citation, and stop + at the section boundary or you silently cross into Constraints. + + Reach for this last. A neighbouring entry is only related to the one you found about half + the time (54% of adjacent pairs share a source line or one within 3), so "read around it" + is a weak way to find the rest of a rule cluster. Two better moves first: + - **The cluster is defined by ID, not by file position.** Landed on HTTP-5 and want the + rule it belongs to? `--req HTTP-3,HTTP-4,HTTP-5`. Appendix C numbers related + requirements together; the topic file does not order them for you. + - **Pull the section.** `--topic X --section rules` — the median section is 7 entries + (~550 tokens). Only the big subsystem `Rules` sections (pipeline 58, retry 43, auth 41) + are expensive enough to need narrowing with bare words. + +## Quick reference + +| Flag | Effect | +|---|---| +| `--req HTTP-7,HTTP-8` | Entries citing any of these. Exact-token: never matches `HTTP-70`. | +| `--topic pipeline,retry` | Topic files by substring — matches broadly and silently. | +| `--section rules,…` | rules, constraints, conclusions, reference, conflicts. (superseded is empty.) | +| `--role spec\|design\|styleguide\|review` | Filter by provenance role. | +| `--chapter 6` | Styleguide chapter. The only way in from a "styleguide N.M" citation. | +| `--grep ` / bare words | Case-insensitive; regex is real, bare words are literal. | +| `--brief` | Drop `` lines, ~30% smaller — but you lose the citation. | +| `--json` | Records, each with a `rollup` boolean. | +| `--list-topics` | The 39 topics with entry and distinct-ID counts. | +| `--list-reqs` | ID → location map. **~6k tokens, bigger than any topic file.** Prefer `--coverage`. | +| `--coverage` | Substantive vs roll-up-only vs uncited, per prefix. A report, not a gate. | + +Zero matches exits 1 and names what does exist — nearest IDs, available topics, harvested +chapters. Follow it rather than guessing again. `--help` for the rest. + +## Citing what you find + +The `` line is the citation, but it comes in two tiers: + +- **spec / design** — repo-relative, quote verbatim: + `` docs/product-spec/09-retry-and-resilience.md:28 · sha:9efbe276001e `` +- **styleguide** — an absolute path to a sibling repo on the harvest machine + (`/home/…/styleguide/typescript/11-testing.md:110-114`). **Strip the machine prefix** + before committing it: `styleguide/typescript/11-testing.md:110-114`. Pasting it raw + produces a citation that resolves on one laptop. + +Shape is not uniform: Conflicts entries carry two sources and no sha; one `review` entry has +no line range. Copy what is there, don't assume four fields. + +Drop `--brief` whenever the result will be cited. + +## Common mistakes + +| Mistake | Fix | +|---|---| +| Trusting a `--req` hit that is all roll-up | Watch for the WARNING; go to appendix C and the owning `product-spec/NN` chapter. | +| Six sequential `--req` calls for one task | One comma-separated call. | +| ID-first on `data-modeling` / `error-handling` / `testing` | Those carry zero IDs. Topic- or chapter-first. | +| Pasting a styleguide `` path verbatim | Strip the machine prefix first. | +| `--topic X` with nothing else | Not a filter; costs more than the file. | +| Reading `--coverage`'s total as "the corpus knows this" | 385/645 are substantive; 256 more are roll-up only. | +| Treating `--coverage` as a gate | Hand-run report. Nothing in CI runs it. | diff --git a/bunfig.toml b/bunfig.toml index dc5be8b..7731a75 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -2,3 +2,7 @@ coverage = true coverageThreshold = 0.8 coverageSkipTestFiles = true +# The 80% floor is a statement about `packages/core`. Scoping discovery to +# `packages` keeps `scripts/*.test.mjs` — repo tooling, run via +# `bun run test:knowledge` (`node --test`) — out of both the run and the floor. +root = "packages" diff --git a/package.json b/package.json index 6aeafe9..e02384f 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "typecheck": "tsc -p packages/core/tsconfig.json --noEmit", "build": "tsc -p packages/core/tsconfig.build.json", "test": "bun test", + "knowledge": "node scripts/knowledge.mjs", + "test:knowledge": "node --test 'scripts/*.test.mjs'", "api": "cd packages/core && bun run api:ci", "lint:publish": "publint packages/core && attw --pack packages/core --ignore-rules cjs-resolves-to-esm", "audit": "bun audit --audit-level=high --prod", diff --git a/scripts/knowledge.mjs b/scripts/knowledge.mjs new file mode 100644 index 0000000..e10150b --- /dev/null +++ b/scripts/knowledge.mjs @@ -0,0 +1,750 @@ +// scripts/knowledge.mjs +// +// Query surface over `docs/knowledge/`. The corpus is 39 topic files and ~1470 +// harvested entries; without a filter, answering "what do we already know about +// RETRY-12" means reading a 20 KB file. This turns that into a query that +// returns the handful of entries that actually cite the requirement. +// +// Not a gate. Nothing in `.github/workflows/` runs this — `--coverage` is a +// report you run by hand when annotating the corpus, not a blocking check. +// +// Zero dependencies, plain Node ESM, same shape as the `verify-*.mjs` scripts. +import {readFileSync, readdirSync} from 'node:fs'; +import {join, basename} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {parseArgs} from 'node:util'; + +const repoRoot = fileURLToPath(new URL('..', import.meta.url)); +const knowledgeDir = join(repoRoot, 'docs', 'knowledge'); +const appendixCPath = join( + repoRoot, + 'docs', + 'product-spec', + 'appendix-c-consolidated-normative-requirement-index.md', +); + +// `INDEX.md` is a generated topic table and `SOURCES.md` a provenance manifest; +// neither holds entries, and both would parse as noise. +const NON_TOPIC_FILES = new Set(['INDEX.md', 'SOURCES.md']); + +// Appendix B is the conformance-test checklist. Its entries roll several +// requirement IDs into one "the suite verifies X, Y, Z" sentence, so they make +// an ID look cited while carrying none of its content. 256 of the 641 cited IDs +// resolve ONLY to a roll-up — a silent wrong answer unless it is called out. +const ROLLUP_SOURCE = 'appendix-b-conformance-test-checklist'; + +// Provenance roles the corpus uses, most common first. +const ROLES = ['spec', 'design', 'styleguide', 'review']; + +// The six sections every harvested topic file carries, in emission order. +const SECTIONS = [ + 'Rules', + 'Constraints', + 'Conclusions', + 'Reference', + 'Conflicts', + 'Superseded', +]; + +// --------------------------------------------------------------------------- +// Canonical requirement IDs +// --------------------------------------------------------------------------- + +// A bare `\b[A-Z]{2,12}-\d+\b` is not a requirement-ID matcher: it also claims +// `UTF-8` (10 hits in the corpus), `SHA-256`, `ISO-8601` and `RFC-3986`. The +// only authority on what a requirement ID looks like is appendix C, so the +// prefix allowlist is derived from it at runtime and never hardcoded — a spec +// revision that adds a subsystem is picked up without editing this file. +const ID_TOKEN = /\b[A-Z][A-Z0-9]{1,11}-\d+\b/g; +const APPENDIX_C_ROW = /^\|\s*([A-Z][A-Z0-9]{1,11}-\d+)\s*\|/; + +function loadCanonicalIds() { + let text; + try { + text = readFileSync(appendixCPath, 'utf8'); + } catch (cause) { + throw new Error( + `cannot read the canonical requirement index at ${appendixCPath}; ` + + 'the requirement-ID allowlist is derived from it and there is no fallback', + {cause}, + ); + } + + const ids = new Map(); + for (const line of text.split('\n')) { + const match = APPENDIX_C_ROW.exec(line); + if (!match) continue; + const cells = line.split('|').map(cell => cell.trim()); + // `| ID | Level | Subsystem | Requirement |` — leading/trailing empties. + ids.set(match[1], { + id: match[1], + level: cells[2] ?? '', + subsystem: cells[3] ?? '', + }); + } + + if (ids.size === 0) { + throw new Error( + `parsed zero requirement IDs out of ${appendixCPath}; its table format ` + + 'changed and the allowlist cannot be derived — refusing to fall back ' + + 'to a bare regex, which false-positives on UTF-8 and SHA-256', + ); + } + return ids; +} + +function derivePrefixes(canonicalIds) { + const prefixes = new Set(); + for (const id of canonicalIds.keys()) { + prefixes.add(id.slice(0, id.lastIndexOf('-'))); + } + return prefixes; +} + +// Tokenize, then compare whole tokens. Never substring-match: `HTTP-7` and +// `HTTP-70` are different requirements and a substring test conflates them. +function extractIds(text, prefixes) { + const found = []; + for (const [token] of text.matchAll(ID_TOKEN)) { + if (!prefixes.has(token.slice(0, token.lastIndexOf('-')))) continue; + if (!found.includes(token)) found.push(token); + } + return found; +} + +// --------------------------------------------------------------------------- +// Corpus parsing +// --------------------------------------------------------------------------- + +const SECTION_HEADING = /^##\s+(.+?)\s*$/; +const BULLET_START = /^-\s+(.*)$/; +const SUB_LINE = /^\s+(.*)<\/sub>\s*$/; +const ROLE_AND_SOURCE = /^(\S+)\s+`(.+)`$/; +const BARE_SOURCE = /^`(.+)`$/; + +// Two `` shapes are in the corpus: +// role · `path:lines` · confidence · sha:xxxx (the common one) +// roleA `pathA` · roleB `pathB` · resolution-status (Conflicts entries) +// so role and path are sometimes separate ` · ` fields and sometimes one. Walk +// the fields and classify each rather than reading them positionally. +function parseSub(inner) { + const roles = []; + const sources = []; + let confidence = null; + let sha = null; + let pendingRole = null; + + for (const raw of inner.split(' · ')) { + const field = raw.trim(); + + const pair = ROLE_AND_SOURCE.exec(field); + if (pair) { + roles.push(pair[1]); + sources.push(pair[2]); + pendingRole = null; + continue; + } + + const bare = BARE_SOURCE.exec(field); + if (bare) { + roles.push(pendingRole ?? 'unknown'); + sources.push(bare[1]); + pendingRole = null; + continue; + } + + if (field.startsWith('sha:')) { + sha = field.slice('sha:'.length); + continue; + } + + // A lone word that is not a confidence level is the role of the source + // field that follows it; anything else is the confidence / status. + if (pendingRole !== null) confidence = pendingRole; + pendingRole = /^\S+$/.test(field) ? field : null; + if (pendingRole === null) confidence = field; + } + if (pendingRole !== null) confidence = pendingRole; + + return { + role: roles[0] ?? null, + roles, + source: sources[0] ?? null, + sources, + confidence, + sha, + }; +} + +function parseFile(path, prefixes) { + const entries = []; + const lines = readFileSync(path, 'utf8').split('\n'); + const file = basename(path); + let section = null; + let current = null; + + const flush = () => { + if (!current) return; + current.reqs = extractIds(current.text, prefixes); + entries.push(current); + current = null; + }; + + for (const [index, line] of lines.entries()) { + const heading = SECTION_HEADING.exec(line); + if (heading) { + flush(); + section = heading[1]; + continue; + } + + const sub = SUB_LINE.exec(line); + if (sub && current) { + Object.assign(current, parseSub(sub[1]), {subLine: line.trim()}); + continue; + } + + const bullet = BULLET_START.exec(line); + if (bullet) { + flush(); + current = { + file, + line: index + 1, + section, + text: bullet[1], + role: null, + roles: [], + source: null, + sources: [], + confidence: null, + sha: null, + subLine: null, + }; + continue; + } + + // A continuation line: any non-`` line before the open bullet's + // provenance line, blank lines included — a few Conflicts entries run to + // several paragraphs, and dropping the tail silently loses the requirement + // IDs it cites. An entry therefore ends only at its ``, at the next + // bullet, at the next heading, or at end of file. + if (current && !current.subLine && line.trim() !== '') { + current.text += ` ${line.trim()}`; + } + } + + flush(); + return entries; +} + +function loadCorpus(prefixes) { + return topicFiles().flatMap(name => + parseFile(join(knowledgeDir, name), prefixes), + ); +} + +function topicFiles() { + return readdirSync(knowledgeDir) + .filter(name => name.endsWith('.md') && !NON_TOPIC_FILES.has(name)) + .sort(); +} + +// True when every source this entry cites is the conformance checklist, i.e. +// the entry names requirement IDs without saying anything about them. +function isRollup(entry) { + return ( + entry.sources.length > 0 && + entry.sources.every(source => source.includes(ROLLUP_SOURCE)) + ); +} + +// Styleguide `` paths carry a numbered chapter file +// (`.../typescript/06-classes-and-data-modeling.md:168-183`), so "styleguide +// 6.7" is answerable by matching the chapter number — no hardcoded chapter → +// topic table that could drift from the styleguide itself. +// Only styleguide-role sources count: `docs/product-spec/04-…md` is a numbered +// chapter too, and conflating the two would answer "styleguide 4" with spec +// chapter 4. `roles[i]` pairs with `sources[i]`. +const STYLEGUIDE_CHAPTER = /\/(\d{2})-[^/]*\.md(?::|$)/; + +function chaptersOf(entry) { + const chapters = []; + entry.sources.forEach((source, index) => { + if (entry.roles[index] !== 'styleguide') return; + const match = STYLEGUIDE_CHAPTER.exec(source); + if (match) chapters.push(String(Number(match[1]))); + }); + return chapters; +} + +// --------------------------------------------------------------------------- +// Filtering +// --------------------------------------------------------------------------- + +function buildFilters(options, positionals, canonicalIds) { + const reqs = (options.req ?? []).flatMap(value => value.split(',')); + for (const id of reqs) { + if (!canonicalIds.has(id)) { + process.stderr.write( + `warning: ${id} is not in appendix C — it is not a canonical ` + + 'requirement ID, so no entry can legitimately cite it\n', + ); + } + } + + const topics = (options.topic ?? []).flatMap(value => value.split(',')); + + const roles = (options.role ?? []) + .flatMap(value => value.split(',')) + .map(value => { + if (!ROLES.includes(value)) { + throw new Error( + `unknown role '${value}'; the roles are ${ROLES.join(', ')}`, + ); + } + return value; + }); + + // "styleguide 6.7" — the chapter is queryable, the sub-section number is not, + // so take the chapter and say plainly that the rest was dropped. + const chapters = (options.chapter ?? []) + .flatMap(value => value.split(',')) + .map(value => { + const match = /^(\d{1,2})(?:\.(\d+))?$/.exec(value.trim()); + if (!match) { + throw new Error( + `unknown chapter '${value}'; expected a styleguide chapter like 6 or 6.7`, + ); + } + if (match[2] !== undefined) { + process.stderr.write( + 'note: entries record a chapter file and line range, not section ' + + `numbers — querying chapter ${match[1]}, ignoring .${match[2]}. ` + + 'Narrow with bare words.\n', + ); + } + return String(Number(match[1])); + }); + + const sections = (options.section ?? []) + .flatMap(value => value.split(',')) + .map(value => { + const resolved = SECTIONS.find( + name => name.toLowerCase() === value.toLowerCase(), + ); + if (!resolved) { + throw new Error( + `unknown section '${value}'; the six sections are ${SECTIONS.join(', ')}`, + ); + } + return resolved; + }); + + const patterns = []; + for (const source of options.grep ?? []) { + patterns.push(new RegExp(source, 'i')); + } + for (const word of positionals) { + patterns.push(new RegExp(escapeRegExp(word), 'i')); + } + + return {reqs, topics, roles, chapters, sections, patterns}; +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +// Every supplied filter must hold — they AND, never OR. Within one filter, +// multiple values OR (`--req A --req B` is "cites A or B"). +function matches(entry, filters) { + const {reqs, topics, roles, chapters, sections, patterns} = filters; + if (reqs.length > 0 && !reqs.some(id => entry.reqs.includes(id))) + return false; + if (topics.length > 0 && !topics.some(t => entry.file.includes(t))) { + return false; + } + if (sections.length > 0 && !sections.includes(entry.section)) return false; + if (roles.length > 0 && !roles.some(r => entry.roles.includes(r))) { + return false; + } + if (chapters.length > 0) { + const entryChapters = chaptersOf(entry); + if (!chapters.some(c => entryChapters.includes(c))) return false; + } + if (!patterns.every(pattern => pattern.test(entry.text))) return false; + return true; +} + +function isEmptyFilter(filters) { + return ( + filters.reqs.length === 0 && + filters.topics.length === 0 && + filters.roles.length === 0 && + filters.chapters.length === 0 && + filters.sections.length === 0 && + filters.patterns.length === 0 + ); +} + +// --------------------------------------------------------------------------- +// Reports +// --------------------------------------------------------------------------- + +function citationIndex(entries) { + const index = new Map(); + for (const entry of entries) { + for (const id of entry.reqs) { + if (!index.has(id)) index.set(id, []); + index.get(id).push(entry); + } + } + return index; +} + +function compareIds(a, b) { + const [prefixA, numberA] = splitId(a); + const [prefixB, numberB] = splitId(b); + return prefixA === prefixB + ? numberA - numberB + : prefixA.localeCompare(prefixB); +} + +function splitId(id) { + const cut = id.lastIndexOf('-'); + return [id.slice(0, cut), Number(id.slice(cut + 1))]; +} + +function renderListReqs(index) { + const out = []; + for (const id of [...index.keys()].sort(compareIds)) { + const locations = index + .get(id) + .map(entry => `${entry.file}:${entry.line}`) + .join(' '); + out.push(`${id}\t${locations}`); + } + out.push(''); + out.push(`${index.size} requirement IDs cited across the corpus`); + return out.join('\n'); +} + +function renderListTopics(entries) { + const stats = new Map(); + for (const entry of entries) { + const name = entry.file.replace(/\.md$/, ''); + if (!stats.has(name)) stats.set(name, {entries: 0, ids: new Set()}); + stats.get(name).entries += 1; + entry.reqs.forEach(id => stats.get(name).ids.add(id)); + } + const out = ['topic\tentries\tdistinct IDs']; + for (const [name, {entries: count, ids}] of [...stats].sort()) { + out.push(`${name}\t${count}\t${ids.size}`); + } + const idless = [...stats].filter(([, v]) => v.ids.size === 0).length; + out.push(''); + out.push( + `${stats.size} topic files. ${idless} carry no requirement ID at all — ` + + 'those are styleguide-derived and are only reachable topic-first.', + ); + return out.join('\n'); +} + +function renderCoverage(canonicalIds, index) { + const uncovered = [...canonicalIds.keys()] + .filter(id => !index.has(id)) + .sort(compareIds); + + const byPrefix = new Map(); + let rollupOnlyTotal = 0; + for (const id of canonicalIds.keys()) { + const [prefix] = splitId(id); + if (!byPrefix.has(prefix)) { + byPrefix.set(prefix, {total: 0, missing: [], rollupOnly: []}); + } + const row = byPrefix.get(prefix); + row.total += 1; + const hits = index.get(id); + if (!hits) { + row.missing.push(id); + } else if (hits.every(isRollup)) { + // Cited, but only by a conformance-checklist sentence that names it. + row.rollupOnly.push(id); + rollupOnlyTotal += 1; + } + } + + const out = ['requirement-ID coverage of docs/knowledge/', '']; + out.push('prefix\tsubstantive\troll-up only\tuncited\ttotal\tuncited IDs'); + for (const prefix of [...byPrefix.keys()].sort()) { + const {total, missing, rollupOnly} = byPrefix.get(prefix); + const substantive = total - missing.length - rollupOnly.length; + out.push( + `${prefix}\t${substantive}\t${rollupOnly.length}\t${missing.length}\t` + + `${total}\t${missing.length === 0 ? '-' : missing.join(' ')}`, + ); + } + const substantiveTotal = + canonicalIds.size - uncovered.length - rollupOnlyTotal; + out.push(''); + out.push( + `${substantiveTotal}/${canonicalIds.size} canonical IDs have a substantive ` + + `entry. ${rollupOnlyTotal} more are named only by an appendix-B ` + + `conformance roll-up (cited, but no content). ${uncovered.length} are ` + + 'cited nowhere.', + ); + return out.join('\n'); +} + +// --------------------------------------------------------------------------- +// Output +// --------------------------------------------------------------------------- + +function renderEntries(results, brief, filters) { + const out = []; + for (const entry of results) { + const tag = isRollup(entry) ? ' [appendix-B roll-up]' : ''; + out.push(`${entry.file}:${entry.line} (${entry.section})${tag}`); + out.push(`- ${entry.text}`); + if (!brief && entry.subLine) out.push(` ${entry.subLine}`); + out.push(''); + } + const files = new Set(results.map(entry => entry.file)); + out.push(`${results.length} entries across ${files.size} topic files`); + + // The silent wrong answer this tool can give: a `--req` that "hits" but whose + // every hit merely names the ID in a conformance-checklist sentence. Exit 0 + // makes it look answered, so say so loudly instead. + if (results.every(isRollup) && (filters?.reqs.length ?? 0) > 0) { + out.push( + '', + 'WARNING: every result is an appendix-B conformance roll-up — it names ' + + `${filters.reqs.join(', ')} without stating the requirement. The corpus ` + + 'has no substantive entry. Read the canonical text in appendix C and ' + + 'the owning docs/product-spec/NN chapter instead.', + ); + } + return out.join('\n'); +} + +// A zero-result query must never look like "the corpus has nothing to say" when +// it is really a typo or the wrong topic name, so spend the tokens on saying +// what nearby things do exist. +function renderNoMatches(filters, entries, index, canonicalIds) { + const out = ['no matching entries.']; + + for (const id of filters.reqs) { + const [prefix, number] = splitId(id); + if (!canonicalIds.has(id)) { + out.push( + ` ${id} is not a canonical requirement ID (not in appendix C).`, + ); + } else { + out.push(` ${id} is canonical but no entry cites it yet.`); + } + const nearest = [...index.keys()] + .filter(other => splitId(other)[0] === prefix) + .sort((a, b) => { + const distance = + Math.abs(splitId(a)[1] - number) - Math.abs(splitId(b)[1] - number); + return distance === 0 ? compareIds(a, b) : distance; + }) + .slice(0, 5); + if (nearest.length > 0) { + out.push(` nearest cited ${prefix} IDs: ${nearest.join(' ')}`); + } else { + const prefixes = [ + ...new Set([...index.keys()].map(id2 => splitId(id2)[0])), + ]; + out.push( + ` no ${prefix} ID is cited anywhere. cited prefixes: ${prefixes.sort().join(' ')}`, + ); + } + const topics = topicsForPrefix(entries, prefix); + if (topics.length > 0) { + out.push(` topics carrying ${prefix} knowledge: ${topics.join(' ')}`); + } + } + + for (const topic of filters.topics) { + const known = [...new Set(entries.map(entry => entry.file))]; + if (!known.some(file => file.includes(topic))) { + out.push( + ` no topic file matches '${topic}'. available: ` + + known.map(file => file.replace(/\.md$/, '')).join(' '), + ); + } + } + + for (const section of filters.sections) { + if (!entries.some(entry => entry.section === section)) { + out.push( + ` the ${section} section is empty across all 39 topic files — ` + + 'nothing has been harvested into it.', + ); + } + } + + for (const chapter of filters.chapters) { + const known = [ + ...new Set(entries.flatMap(entry => chaptersOf(entry))), + ].sort((a, b) => Number(a) - Number(b)); + if (!known.includes(chapter)) { + out.push( + ` no entry cites styleguide chapter ${chapter}. harvested ` + + `chapters: ${known.join(' ')}`, + ); + } + } + + if (filters.patterns.length > 0 && filters.reqs.length === 0) { + out.push( + ' text filters are applied to entry text only; try --grep with a ' + + 'looser pattern, or drop --section/--topic.', + ); + } + return out.join('\n'); +} + +function topicsForPrefix(entries, prefix) { + const counts = new Map(); + for (const entry of entries) { + for (const id of entry.reqs) { + if (splitId(id)[0] !== prefix) continue; + counts.set(entry.file, (counts.get(entry.file) ?? 0) + 1); + } + } + return [...counts.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, 3) + .map(([file]) => file.replace(/\.md$/, '')); +} + +const USAGE = `Usage: bun run knowledge [options] [words...] + +Query docs/knowledge/. Different filters AND together; values within one filter OR. + + --req entries citing that requirement ID (repeatable, comma-ok). + Comma form is the whole-task query: --req HTTP-13,HTTP-14 + --topic topic files whose name contains any of these (substring) + --section ${SECTIONS.map(s => s.toLowerCase()).join(' | ')} + --role ${ROLES.join(' | ')} + --chapter styleguide chapter, e.g. 6 (a "6.7" drops the .7) + --grep case-insensitive regex over entry text (repeatable) + bare words: case-insensitive substrings, all must match + --brief drop provenance lines (~30% less output) + --json machine-readable records + --list-topics the 39 topics with entry and distinct-ID counts + --list-reqs requirement-ID -> location map (~6k tokens; prefer --coverage) + --coverage substantive vs roll-up-only vs uncited, per prefix + --help + +Exits 1 when a query matches nothing. + +Examples: + bun run knowledge --req HTTP-13,HTTP-14,HTTP-15 # one task's whole ID set + bun run knowledge --chapter 6 interface class # "styleguide 6.7" + bun run knowledge --section conflicts --brief # open design-vs-styleguide calls + bun run knowledge --topic pipeline --section rules --brief cursor fork +`; + +function main(argv) { + const {values, positionals} = parseArgs({ + args: argv, + allowPositionals: true, + options: { + req: {type: 'string', multiple: true}, + topic: {type: 'string', multiple: true}, + section: {type: 'string', multiple: true}, + role: {type: 'string', multiple: true}, + chapter: {type: 'string', multiple: true}, + grep: {type: 'string', multiple: true}, + brief: {type: 'boolean', default: false}, + json: {type: 'boolean', default: false}, + 'list-topics': {type: 'boolean', default: false}, + 'list-reqs': {type: 'boolean', default: false}, + coverage: {type: 'boolean', default: false}, + help: {type: 'boolean', default: false}, + }, + }); + + if (values.help) { + process.stdout.write(USAGE); + return 0; + } + + const canonicalIds = loadCanonicalIds(); + const entries = loadCorpus(derivePrefixes(canonicalIds)); + const index = citationIndex(entries); + + if (values['list-topics']) { + process.stdout.write(`${renderListTopics(entries)}\n`); + return 0; + } + if (values['list-reqs']) { + process.stdout.write(`${renderListReqs(index)}\n`); + return 0; + } + if (values.coverage) { + process.stdout.write(`${renderCoverage(canonicalIds, index)}\n`); + return 0; + } + + const filters = buildFilters(values, positionals, canonicalIds); + if (isEmptyFilter(filters)) { + process.stdout.write(USAGE); + return 0; + } + + const results = entries.filter(entry => matches(entry, filters)); + + if (values.json) { + const annotated = results.map(entry => ({ + ...entry, + rollup: isRollup(entry), + })); + process.stdout.write(`${JSON.stringify(annotated, null, 2)}\n`); + return results.length === 0 ? 1 : 0; + } + + if (results.length === 0) { + process.stdout.write( + `${renderNoMatches(filters, entries, index, canonicalIds)}\n`, + ); + return 1; + } + + process.stdout.write(`${renderEntries(results, values.brief, filters)}\n`); + return 0; +} + +export { + loadCanonicalIds, + derivePrefixes, + extractIds, + parseSub, + parseFile, + loadCorpus, + citationIndex, + buildFilters, + matches, + renderCoverage, + renderEntries, + renderListTopics, + isRollup, + chaptersOf, + topicFiles, + compareIds, + main, +}; + +// Only run the CLI when invoked directly, so the test file can import the +// parsing helpers without the process exiting underneath it. +if (process.argv[1] === fileURLToPath(import.meta.url)) { + try { + process.exitCode = main(process.argv.slice(2)); + } catch (error) { + process.stderr.write(`${error.message}\n`); + process.exitCode = 2; + } +} diff --git a/scripts/knowledge.test.mjs b/scripts/knowledge.test.mjs new file mode 100644 index 0000000..bb3da0e --- /dev/null +++ b/scripts/knowledge.test.mjs @@ -0,0 +1,375 @@ +// scripts/knowledge.test.mjs +// +// Run with `bun run test:knowledge` (`node --test 'scripts/*.test.mjs'` — Node +// 26 no longer accepts a bare directory there). Deliberately outside `bun test`, +// which `bunfig.toml` scopes to `packages`: the 80% line-coverage floor is a +// statement about `packages/core`, not about repo tooling. +import assert from 'node:assert/strict'; +import {mkdtempSync, writeFileSync} from 'node:fs'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {test} from 'node:test'; + +import { + buildFilters, + citationIndex, + compareIds, + derivePrefixes, + extractIds, + loadCanonicalIds, + loadCorpus, + matches, + parseFile, + parseSub, + renderCoverage, + renderEntries, + renderListTopics, + isRollup, + chaptersOf, + topicFiles, +} from './knowledge.mjs'; + +const canonicalIds = loadCanonicalIds(); +const prefixes = derivePrefixes(canonicalIds); + +function fixture(body) { + const dir = mkdtempSync(join(tmpdir(), 'knowledge-test-')); + const path = join(dir, 'topic.md'); + writeFileSync(path, body); + return path; +} + +// --- canonical IDs and the derived allowlist ------------------------------- + +test('appendix C parses into the full canonical requirement set', () => { + assert.equal(canonicalIds.size, 645); + assert.equal(canonicalIds.get('SEAM-1').level, 'MUST'); + assert.equal(canonicalIds.get('RETRY-12').level, 'SHOULD'); + assert.ok(canonicalIds.has('HTTP-7')); +}); + +test('the prefix allowlist is derived from appendix C, not hardcoded', () => { + assert.equal(prefixes.size, 19); + for (const prefix of ['HTTP', 'SEAM', 'RETRY', 'CTX', 'NFR']) { + assert.ok(prefixes.has(prefix), `${prefix} should be a canonical prefix`); + } +}); + +test('the allowlist rejects the shapes a bare regex false-positives on', () => { + const text = + 'Encode as UTF-8, hash with SHA-256, per RFC-3986 and ISO-8601, see HTTP-7.'; + assert.deepEqual(extractIds(text, prefixes), ['HTTP-7']); + for (const prefix of ['UTF', 'SHA', 'RFC', 'ISO']) { + assert.ok(!prefixes.has(prefix), `${prefix} must not be an ID prefix`); + } +}); + +test('ID extraction is exact-token, so HTTP-7 does not match HTTP-70', () => { + const found = extractIds( + 'Covers HTTP-70 and HTTP-700 but not the short one.', + prefixes, + ); + assert.deepEqual(found, ['HTTP-70', 'HTTP-700']); + assert.ok(!found.includes('HTTP-7')); +}); + +test('ID extraction de-duplicates and preserves first-seen order', () => { + assert.deepEqual( + extractIds('SEAM-29 then HTTP-2 then SEAM-29 again.', prefixes), + ['SEAM-29', 'HTTP-2'], + ); +}); + +// --- entry parsing --------------------------------------------------------- + +test('entries are attributed to the section heading above them', () => { + const path = fixture( + [ + '# topic', + '', + '## Rules', + '- First rule (HTTP-1).', + ' spec · `docs/product-spec/04-core-http-domain-model.md:9` · high · sha:abc123', + '', + '## Constraints', + '- A constraint (SEAM-1).', + ' design · `docs/sdk-design-nodejs/02-package-and-workspace-layout.md:3` · high · sha:def456', + '', + ].join('\n'), + ); + const entries = parseFile(path, prefixes); + + assert.equal(entries.length, 2); + assert.deepEqual( + entries.map(entry => [entry.section, entry.line, entry.reqs]), + [ + ['Rules', 4, ['HTTP-1']], + ['Constraints', 8, ['SEAM-1']], + ], + ); + assert.equal(entries[0].role, 'spec'); + assert.equal( + entries[0].source, + 'docs/product-spec/04-core-http-domain-model.md:9', + ); + assert.equal(entries[0].confidence, 'high'); + assert.equal(entries[0].sha, 'abc123'); +}); + +test('a multi-paragraph bullet keeps its tail, blank lines included', () => { + const path = fixture( + [ + '## Conflicts', + '- Opening claim.', + '', + ' Continuation paragraph citing PAGE-11 after a blank line.', + ' review · `docs/superpowers/specs/x.md` · high · sha:manual', + '', + ].join('\n'), + ); + const [entry] = parseFile(path, prefixes); + + assert.match(entry.text, /Continuation paragraph/); + assert.deepEqual(entry.reqs, ['PAGE-11']); + assert.equal(entry.section, 'Conflicts'); +}); + +test('a two-source Conflicts yields both role/source pairs', () => { + const parsed = parseSub( + 'design `docs/sdk-design-nodejs/04.md:7-14` · styleguide `/abs/06-classes.md:168-183` · unresolved 2026-07-25', + ); + assert.deepEqual(parsed.roles, ['design', 'styleguide']); + assert.deepEqual(parsed.sources, [ + 'docs/sdk-design-nodejs/04.md:7-14', + '/abs/06-classes.md:168-183', + ]); + assert.equal(parsed.confidence, 'unresolved 2026-07-25'); + assert.equal(parsed.sha, null); +}); + +test('the standard four-field splits role from source correctly', () => { + const parsed = parseSub( + 'spec · `docs/product-spec/09-retry-and-resilience.md:28` · high · sha:9efbe276001e', + ); + assert.deepEqual(parsed.roles, ['spec']); + assert.equal( + parsed.source, + 'docs/product-spec/09-retry-and-resilience.md:28', + ); + assert.equal(parsed.confidence, 'high'); + assert.equal(parsed.sha, '9efbe276001e'); +}); + +test('the real corpus parses with one per bullet and no orphans', () => { + const entries = loadCorpus(prefixes); + assert.equal(entries.length, 1470); + for (const entry of entries) { + assert.ok(entry.subLine, `${entry.file}:${entry.line} lost its line`); + assert.ok( + entry.sources.length > 0, + `${entry.file}:${entry.line} has no source`, + ); + assert.ok(entry.section, `${entry.file}:${entry.line} has no section`); + } +}); + +// --- filtering ------------------------------------------------------------- + +const sampleEntry = { + file: 'retry-and-resilience.md', + line: 8, + section: 'Rules', + text: 'The retryable-status classifier MUST be single-sourced (RETRY-1).', + roles: ['spec'], + reqs: ['RETRY-1'], +}; + +function filtersFor(values, positionals = []) { + return buildFilters(values, positionals, canonicalIds); +} + +test('--req matches on the exact token only', () => { + assert.ok(matches(sampleEntry, filtersFor({req: ['RETRY-1']}))); + assert.ok(!matches(sampleEntry, filtersFor({req: ['RETRY-10']}))); +}); + +test('filters AND together across dimensions', () => { + const both = filtersFor({req: ['RETRY-1'], section: ['rules']}); + assert.ok(matches(sampleEntry, both)); + + const sectionMiss = filtersFor({req: ['RETRY-1'], section: ['reference']}); + assert.ok(!matches(sampleEntry, sectionMiss)); + + const roleMiss = filtersFor({req: ['RETRY-1'], role: ['styleguide']}); + assert.ok(!matches(sampleEntry, roleMiss)); + + const topicMiss = filtersFor({req: ['RETRY-1'], topic: ['pagination']}); + assert.ok(!matches(sampleEntry, topicMiss)); +}); + +test('multiple values within one filter OR together', () => { + const either = filtersFor({req: ['RETRY-99', 'RETRY-1']}); + assert.ok(matches(sampleEntry, either)); +}); + +test('bare words AND together and are case-insensitive', () => { + assert.ok( + matches(sampleEntry, filtersFor({}, ['CLASSIFIER', 'single-sourced'])), + ); + assert.ok(!matches(sampleEntry, filtersFor({}, ['classifier', 'redirect']))); +}); + +test('an unknown --section name fails loudly rather than matching nothing', () => { + assert.throws( + () => filtersFor({section: ['rulez']}), + /unknown section 'rulez'/, + ); +}); + +// --- reports --------------------------------------------------------------- + +test('a cited ID never appears in the uncited column', () => { + const index = citationIndex(loadCorpus(prefixes)); + const uncited = [...canonicalIds.keys()].filter(id => !index.has(id)); + for (const id of uncited) { + assert.equal(index.get(id), undefined); + } + assert.ok(index.has('RETRY-13'), 'RETRY-13 should be cited after annotation'); +}); + +test('IDs sort by prefix then numerically, not lexically', () => { + const sorted = ['HTTP-70', 'HTTP-7', 'AUTH-2', 'HTTP-100'].sort(compareIds); + assert.deepEqual(sorted, ['AUTH-2', 'HTTP-7', 'HTTP-70', 'HTTP-100']); +}); + +// --- roll-up detection ------------------------------------------------------ + +test('an entry sourced only from appendix B is a roll-up', () => { + const rollup = { + sources: ['docs/product-spec/appendix-b-conformance-test-checklist.md:91'], + }; + const substantive = { + sources: ['docs/product-spec/09-retry-and-resilience.md:28'], + }; + const mixed = {sources: [...rollup.sources, ...substantive.sources]}; + + assert.ok(isRollup(rollup)); + assert.ok(!isRollup(substantive)); + assert.ok(!isRollup(mixed), 'one real source is enough to be substantive'); + assert.ok(!isRollup({sources: []})); +}); + +test('a --req answered only by roll-ups warns instead of exiting quietly', () => { + const entries = loadCorpus(prefixes); + const index = citationIndex(entries); + const hits = index.get('NFR-13'); + + assert.ok(hits.every(isRollup), 'NFR-13 is roll-up-only in this corpus'); + const rendered = renderEntries(hits, false, {reqs: ['NFR-13']}); + assert.match(rendered, /\[appendix-B roll-up\]/); + assert.match(rendered, /WARNING: every result is an appendix-B/); + assert.match(rendered, /NFR-13/); +}); + +test('a substantive result carries no roll-up warning', () => { + const index = citationIndex(loadCorpus(prefixes)); + const rendered = renderEntries(index.get('RETRY-13'), false, { + reqs: ['RETRY-13'], + }); + assert.ok(!rendered.includes('WARNING')); + assert.ok(!rendered.includes('[appendix-B roll-up]')); +}); + +test('coverage separates substantive from roll-up-only from uncited', () => { + const index = citationIndex(loadCorpus(prefixes)); + const report = renderCoverage(canonicalIds, index); + + const rows = report + .split('\n') + .map(line => /^([A-Z][A-Z0-9]*)\t(\d+)\t(\d+)\t(\d+)\t(\d+)\t/.exec(line)) + .filter(Boolean); + assert.equal(rows.length, prefixes.size); + + let total = 0; + for (const [, prefix, sub, rollup, uncited, rowTotal] of rows) { + assert.equal( + Number(sub) + Number(rollup) + Number(uncited), + Number(rowTotal), + `${prefix}: the three buckets must partition the total`, + ); + total += Number(rowTotal); + } + assert.equal(total, canonicalIds.size); + assert.match(report, /\d+\/645 canonical IDs have a substantive entry/); +}); + +// --- styleguide chapters ---------------------------------------------------- + +test('chapters come from styleguide sources only, never spec chapters', () => { + const specOnly = { + roles: ['spec'], + sources: ['docs/product-spec/04-core-http-domain-model.md:22-22'], + }; + assert.deepEqual( + chaptersOf(specOnly), + [], + 'spec chapter 04 is not chapter 4', + ); + + const styleguide = { + roles: ['styleguide'], + sources: [ + '/home/u/styleguide/typescript/06-classes-and-data-modeling.md:168-183', + ], + }; + assert.deepEqual(chaptersOf(styleguide), ['6'], 'leading zero is stripped'); + + const conflict = { + roles: ['design', 'styleguide'], + sources: [ + 'docs/sdk-design-nodejs/09-toolchain-and-quality-gates.md:12', + '/home/u/styleguide/typescript/11-testing.md:47-48', + ], + }; + assert.deepEqual(chaptersOf(conflict), ['11'], 'only the styleguide side'); +}); + +test('--chapter 6 reaches data-modeling, which carries no requirement ID', () => { + const entries = loadCorpus(prefixes); + const filters = filtersFor({chapter: ['6.7']}); + assert.deepEqual( + filters.chapters, + ['6'], + 'the sub-section number is dropped', + ); + + const hits = entries.filter(entry => matches(entry, filters)); + assert.ok(hits.length > 0); + assert.ok(hits.some(entry => entry.file === 'data-modeling.md')); + assert.ok( + hits.every( + entry => entry.reqs.length === 0 || entry.roles.includes('styleguide'), + ), + ); +}); + +test('an unknown chapter or role fails loudly, like an unknown section', () => { + assert.throws(() => filtersFor({chapter: ['six']}), /unknown chapter 'six'/); + assert.throws(() => filtersFor({role: ['spek']}), /unknown role 'spek'/); +}); + +// --- topic listing ---------------------------------------------------------- + +test('--list-topics covers every topic file and counts the ID-less ones', () => { + const entries = loadCorpus(prefixes); + const report = renderListTopics(entries); + + assert.equal(topicFiles().length, 39); + for (const name of topicFiles()) { + assert.ok( + report.includes(name.replace(/\.md$/, '')), + `${name} missing from --list-topics`, + ); + } + assert.match(report, /39 topic files\. 16 carry no requirement ID at all/); +}); From 94895ea25cf065627fb5e5620b8c0cb0da0a16a8 Mon Sep 17 00:00:00 2001 From: Mohammad Wahbeh Date: Wed, 26 Aug 2026 01:30:59 +0300 Subject: [PATCH 2/2] docs(knowledge): cite governing requirement IDs on harvested entries 194 of appendix C's 645 IDs had no entry naming them, concentrated in six subsystems: RETRY 41, AUTH 36, RECOV 29, PIPE 27, REDIR 25, CTX 20. That was a citation gap, not a knowledge gap -- retry-and-resilience.md already held 68 entries and authentication.md 49; the entries simply did not name the IDs they govern, so `--req RETRY-12` found nothing on the subsystems most in need of lookup. Each entry's line records the spec file and line range it was harvested from, and those lines carry the bolded requirement IDs, so candidates were derived from the source rather than guessed, then matched one by one against appendix C's per-ID text. RETRY, AUTH, PIPE, REDIR and CTX now reach full coverage. Four IDs were left alone as genuine knowledge gaps rather than annotated to make the number look better: RECOV-32 (idempotency-key step), RECOV-33 (client- identity step), RECOV-34 (retry-config validation) and SEAM-28 (the projection's operation identifier) have no entry stating their content. RECOV-17..34 are defined only in appendix C -- no numbered chapter body carries them -- so they restate the recovery stack's contract that chapter 09 expresses as RETRY-*. Those are annotated as pairs, e.g. "(RETRY-27 / RECOV-20)". Structure is unchanged and stays byte-compatible with what /knowledge-harvest emits: 1470 bullets, 1470 lines, 234 sections. Every one of the 234 changed lines is a "- " bullet; no provenance line, source path, line range or sha was touched. Note that this is a hand-authored pass, not generated output -- nothing regenerates it, and a future re-harvest of an annotated topic file would overwrite these citations. CLAUDE.md's knowledge-query section is included here because its substantive-vs-roll-up figures (385/256/4) only read true once this pass has landed. --- CLAUDE.md | 24 +++++ docs/knowledge/authentication.md | 82 +++++++------- docs/knowledge/concurrency-and-async.md | 2 +- docs/knowledge/execution-context.md | 64 +++++------ docs/knowledge/message-bodies.md | 4 +- docs/knowledge/pagination.md | 4 +- docs/knowledge/pipeline.md | 126 +++++++++++----------- docs/knowledge/redirect-handling.md | 60 +++++------ docs/knowledge/retry-and-resilience.md | 102 +++++++++--------- docs/knowledge/seams-and-extensibility.md | 10 +- docs/knowledge/serde.md | 10 +- docs/knowledge/sse-streaming.md | 4 +- 12 files changed, 258 insertions(+), 234 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 07d77fc..beaa561 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,6 +66,26 @@ Four distinct trees, easy to confuse: `docs/product-spec/appendix-c-consolidated-normative-requirement-index.md` is the fastest way to locate a requirement ID. +### Querying `docs/knowledge/` + +`docs/knowledge/` is 518 KB across 39 topic files — never read a topic file whole when a filtered query +answers the question. `bun run knowledge` parses the corpus into entries and filters them; a requirement-ID +query returns ~170 tokens against a ~5700-token file read. + +```bash +bun run knowledge --req HTTP-13,HTTP-14,HTTP-15 # a whole task's IDs in one call (exact-token) +bun run knowledge --chapter 6 interface class # a "styleguide 6.7" citation +bun run knowledge --section conflicts --brief # open design-vs-styleguide calls; 6 entries corpus-wide +``` + +Different filters AND together, values within one filter OR; `--help` lists the rest. Each result carries its +`` provenance line — the citation for test-file headers and deferral notes, though styleguide paths are +absolute to a sibling repo and need their machine prefix stripped first. **A `--req` hit is not proof of +knowledge:** 256 of 645 IDs are named only by an appendix-B conformance roll-up, tagged `[appendix-B roll-up]` +in output; only 385 have a substantive entry (`--coverage` breaks this down). 16 of the 39 topics carry no +requirement ID at all and are reachable only via `--topic`/`--chapter` (`--list-topics`). Nothing in CI runs +this. The `.claude/skills/knowledge-lookup` skill carries the full workflow. + ## Requirement-ID conventions (enforced by review, not tooling) - Every source file opens with `// SPDX-License-Identifier: MIT` on **line 1** (NFR-13). @@ -139,3 +159,7 @@ Work proceeds phase by phase against `docs/superpowers/specs/2026-07-23-nodejs-s phase has a design spec, an implementation plan with numbered tasks (TDD: write the failing test, confirm it fails, implement, confirm it passes, commit), and a checklist mapping every requirement ID to the task that satisfies it. When asked to implement or validate a phase, read all three before touching code. + +Starting a numbered task means starting with what the corpus already knows about its requirement IDs — invoke +the `knowledge-lookup` skill, which carries both entry points (ID-first via appendix C, topic-first for the +styleguide-derived areas that carry no IDs). diff --git a/docs/knowledge/authentication.md b/docs/knowledge/authentication.md index d85c0a3..227810e 100644 --- a/docs/knowledge/authentication.md +++ b/docs/knowledge/authentication.md @@ -3,83 +3,83 @@ ## Rules - A port must preserve the security invariants of never leaking credentials over plaintext or cross-origin, an unpredictable Digest cnonce, and secret redaction, along with the deterministic resolution semantics and the exact challenge/retry lifecycle. spec · `docs/product-spec/11-authentication.md:3` · high · sha:efba58233dd1 -- The recognized auth scheme set MUST be exactly {OAUTH2, API_KEY, BASIC, DIGEST, NO_AUTH}, where NO_AUTH is a distinct sentinel meaning 'may run anonymously / skip credential stamping' rather than a wire scheme. +- The recognized auth scheme set MUST be exactly {OAUTH2, API_KEY, BASIC, DIGEST, NO_AUTH}, where NO_AUTH is a distinct sentinel meaning 'may run anonymously / skip credential stamping' rather than a wire scheme (AUTH-1). spec · `docs/product-spec/11-authentication.md:7` · high · sha:efba58233dd1 -- An auth requirement MUST bind exactly one scheme to its own OAuth scopes and params, meaningful only for OAUTH2 and never inspected by resolution but preserved, MUST be immutable such that input collections mutated after construction do not affect the stored value, and MUST have value-based equality over scheme, scopes, and params. +- An auth requirement MUST bind exactly one scheme to its own OAuth scopes and params, meaningful only for OAUTH2 and never inspected by resolution but preserved, MUST be immutable such that input collections mutated after construction do not affect the stored value, and MUST have value-based equality over scheme, scopes, and params (AUTH-2). spec · `docs/product-spec/11-authentication.md:7` · high · sha:efba58233dd1 -- An auth descriptor MUST be a non-empty ordered list of requirements in preference order, MUST reject an empty list at construction, MUST be immutable, and MUST report 'allows anonymous' true if and only if any requirement's scheme is NO_AUTH. +- An auth descriptor MUST be a non-empty ordered list of requirements in preference order, MUST reject an empty list at construction, MUST be immutable, and MUST report 'allows anonymous' true if and only if any requirement's scheme is NO_AUTH (AUTH-3). spec · `docs/product-spec/11-authentication.md:7` · high · sha:efba58233dd1 -- Tier resolution MUST select the single most-specific descriptor present, in the strict order per-call, then operation, then client, and resolve only against that descriptor; a higher tier that is present but unsatisfiable MUST NOT fall through to a lower tier, since it fails because the caller asked for that override explicitly. +- Tier resolution MUST select the single most-specific descriptor present, in the strict order per-call, then operation, then client, and resolve only against that descriptor; a higher tier that is present but unsatisfiable MUST NOT fall through to a lower tier, since it fails because the caller asked for that override explicitly (AUTH-4). spec · `docs/product-spec/11-authentication.md:8` · high · sha:efba58233dd1 -- Within the selected auth descriptor, resolution MUST return the first requirement in declared order whose scheme is satisfiable, where satisfiable means NO_AUTH (always) or membership in the supplied set of available schemes, without inspecting any concrete credential. +- Within the selected auth descriptor, resolution MUST return the first requirement in declared order whose scheme is satisfiable, where satisfiable means NO_AUTH (always) or membership in the supplied set of available schemes, without inspecting any concrete credential (AUTH-5). spec · `docs/product-spec/11-authentication.md:8` · high · sha:efba58233dd1 -- Auth resolution MUST fail with an argument error when all tiers are absent, and with a distinct auth-resolution error carrying the required schemes in preference order and the available schemes when the selected descriptor lists no satisfiable scheme. +- Auth resolution MUST fail with an argument error when all tiers are absent, and with a distinct auth-resolution error carrying the required schemes in preference order and the available schemes when the selected descriptor lists no satisfiable scheme (AUTH-6). spec · `docs/product-spec/11-authentication.md:8` · high · sha:efba58233dd1 -- The auth resolver MUST be stateless, concurrency-safe, and a deterministic pure function of its inputs. +- The auth resolver MUST be stateless, concurrency-safe, and a deterministic pure function of its inputs (AUTH-7). spec · `docs/product-spec/11-authentication.md:8` · high · sha:efba58233dd1 -- Every credential type MUST redact its secret in any string/diagnostic representation without mutating or corrupting the real fields, MAY leave non-secret fields visible, and MUST preserve its variant-specific equality -- the bearer token has value-based equality over its real token and expiry (unaffected by the redacted string form), while the API-key and name-key credentials use reference identity, so two instances with identical fields are not equal. +- Every credential type MUST redact its secret in any string/diagnostic representation without mutating or corrupting the real fields, MAY leave non-secret fields visible, and MUST preserve its variant-specific equality -- the bearer token has value-based equality over its real token and expiry (unaffected by the redacted string form), while the API-key and name-key credentials use reference identity, so two instances with identical fields are not equal (AUTH-8). spec · `docs/product-spec/11-authentication.md:8` · high · sha:efba58233dd1 -- Credential construction MUST validate secret and identity fields as non-blank and reject blanks, for the bearer token, API key, and name-key name and key. +- Credential construction MUST validate secret and identity fields as non-blank and reject blanks, for the bearer token, API key, and name-key name and key (AUTH-9). spec · `docs/product-spec/11-authentication.md:8` · high · sha:efba58233dd1 -- Bearer-token expiry MUST be optional, with null meaning it never locally expires, and MUST be evaluated additively with a grace margin -- expired at reference time now with margin M if and only if expiry is non-null and now plus M is strictly after expiry. +- Bearer-token expiry MUST be optional, with null meaning it never locally expires, and MUST be evaluated additively with a grace margin -- expired at reference time now with margin M if and only if expiry is non-null and now plus M is strictly after expiry (AUTH-10). spec · `docs/product-spec/11-authentication.md:8` · high · sha:efba58233dd1 -- A token provider's fetch errors MUST propagate and MUST NOT be cached, so a subsequent request retries, and async callers MUST observe a provider error through the asynchronous channel (a failed future), never a synchronous throw. +- A token provider's fetch errors MUST propagate and MUST NOT be cached, so a subsequent request retries, and async callers MUST observe a provider error through the asynchronous channel (a failed future), never a synchronous throw (AUTH-11). spec · `docs/product-spec/11-authentication.md:8` · high · sha:efba58233dd1 -- The challenge parser MUST parse RFC 7235 WWW-Authenticate/Proxy-Authenticate values into an ordered list of challenges, honoring multiple comma-separated challenges, quoted-string values containing commas and equals signs, backslash escapes, scheme/param names normalized to lower case, values stored verbatim after unquoting, a bare scheme emitted with an empty parameter map, and a token68 value recorded under a synthetic key. +- The challenge parser MUST parse RFC 7235 WWW-Authenticate/Proxy-Authenticate values into an ordered list of challenges, honoring multiple comma-separated challenges, quoted-string values containing commas and equals signs, backslash escapes, scheme/param names normalized to lower case, values stored verbatim after unquoting, a bare scheme emitted with an empty parameter map, and a token68 value recorded under a synthetic key (AUTH-12). spec · `docs/product-spec/11-authentication.md:16` · high · sha:efba58233dd1 -- The challenge parser MUST be lenient and never throw -- blank input yields an empty list, a malformed challenge recovers to the next top-level comma, an unterminated quoted string terminates at end-of-input, and parameters parsed before a malformed tail are preserved. +- The challenge parser MUST be lenient and never throw -- blank input yields an empty list, a malformed challenge recovers to the next top-level comma, an unterminated quoted string terminates at end-of-input, and parameters parsed before a malformed tail are preserved (AUTH-13). spec · `docs/product-spec/11-authentication.md:16` · high · sha:efba58233dd1 -- Basic stamping MUST produce 'Basic ' plus base64 of UTF-8-encoded username:password, computed once, accept a Basic challenge case-insensitively, emit Authorization or Proxy-Authorization for a proxy challenge, and validate credentials as non-empty, permitting whitespace-only per RFC 7617, which is laxer than the non-blank rule used elsewhere. +- Basic stamping MUST produce 'Basic ' plus base64 of UTF-8-encoded username:password, computed once, accept a Basic challenge case-insensitively, emit Authorization or Proxy-Authorization for a proxy challenge, and validate credentials as non-empty, permitting whitespace-only per RFC 7617, which is laxer than the non-blank rule used elsewhere (AUTH-14). spec · `docs/product-spec/11-authentication.md:17` · high · sha:efba58233dd1 -- Digest stamping MUST support exactly {MD5, MD5-sess, SHA-256, SHA-256-sess} with qop auth or absent, declining auth-int-only challenges, unsupported algorithms, and mutual-auth verification. +- Digest stamping MUST support exactly {MD5, MD5-sess, SHA-256, SHA-256-sess} with qop auth or absent, declining auth-int-only challenges, unsupported algorithms, and mutual-auth verification (AUTH-15). spec · `docs/product-spec/11-authentication.md:18` · high · sha:efba58233dd1 -- A Digest challenge is considered satisfiable if and only if the scheme is Digest (case-insensitive), it carries realm and nonce, qop contains auth or is absent, and the algorithm is supported or absent, defaulting to MD5, preferring the algorithm earliest in the configured preference list regardless of wire order. +- A Digest challenge is considered satisfiable if and only if the scheme is Digest (case-insensitive), it carries realm and nonce, qop contains auth or is absent, and the algorithm is supported or absent, defaulting to MD5, preferring the algorithm earliest in the configured preference list regardless of wire order (AUTH-16). spec · `docs/product-spec/11-authentication.md:18` · high · sha:efba58233dd1 -- Digest stamping MUST compute HA1/HA2/response per RFC 7616/2069 using lower-case hex of the selected algorithm. +- Digest stamping MUST compute HA1/HA2/response per RFC 7616/2069 using lower-case hex of the selected algorithm (AUTH-17). spec · `docs/product-spec/11-authentication.md:18` · high · sha:efba58233dd1 -- The Digest nonce count MUST be tracked per server nonce starting at 00000001 and incrementing only on reuse, rendered as exactly 8 lower-case hex digits using the low 32 bits on overflow. +- The Digest nonce count MUST be tracked per server nonce starting at 00000001 and incrementing only on reuse, rendered as exactly 8 lower-case hex digits using the low 32 bits on overflow (AUTH-18). spec · `docs/product-spec/11-authentication.md:18` · high · sha:efba58233dd1 -- The Digest client nonce MUST be drawn from a cryptographically strong source with at least 128 bits of entropy. +- The Digest client nonce MUST be drawn from a cryptographically strong source with at least 128 bits of entropy (AUTH-20). spec · `docs/product-spec/11-authentication.md:18` · high · sha:efba58233dd1 -- Digest MUST use UTF-8 hash-input encoding when the challenge advertises charset=UTF-8 and ISO-8859-1 otherwise. +- Digest MUST use UTF-8 hash-input encoding when the challenge advertises charset=UTF-8 and ISO-8859-1 otherwise (AUTH-21). spec · `docs/product-spec/11-authentication.md:18` · high · sha:efba58233dd1 -- Digest stamping MUST quote/escape the appropriate fields, leave qop/nc/algorithm unquoted with the full algorithm spelling, use the request-target as the digest-uri, and emit cnonce/nc/qop only when qop is negotiated. +- Digest stamping MUST quote/escape the appropriate fields, leave qop/nc/algorithm unquoted with the full algorithm spelling, use the request-target as the digest-uri, and emit cnonce/nc/qop only when qop is negotiated (AUTH-22). spec · `docs/product-spec/11-authentication.md:18` · high · sha:efba58233dd1 -- The per-nonce counter store SHOULD be bounded, defaulting to 1024 entries, and drained under the cap; evicting a live nonce is harmless because its nc restarts at 1, which is spec-legal for a fresh nonce. +- The per-nonce counter store SHOULD be bounded, defaulting to 1024 entries, and drained under the cap; evicting a live nonce is harmless because its nc restarts at 1, which is spec-legal for a fresh nonce (AUTH-19). spec · `docs/product-spec/11-authentication.md:18` · high · sha:efba58233dd1 -- Composing auth handlers MUST delegate to the first handler in declaration order whose can-handle check passes and MUST defensively copy the handler list; callers order stronger schemes first. +- Composing auth handlers MUST delegate to the first handler in declaration order whose can-handle check passes and MUST defensively copy the handler list; callers order stronger schemes first (AUTH-23). spec · `docs/product-spec/11-authentication.md:19` · high · sha:efba58233dd1 -- Auth handlers MUST be safe for concurrent invocation, with per-handler mutable counters such as Digest nc using thread-safe primitives so concurrent reuse of one nonce still yields correct, non-duplicated counts. +- Auth handlers MUST be safe for concurrent invocation, with per-handler mutable counters such as Digest nc using thread-safe primitives so concurrent reuse of one nonce still yields correct, non-duplicated counts (AUTH-24). spec · `docs/product-spec/11-authentication.md:19` · high · sha:efba58233dd1 -- An auth handler MUST emit Authorization for WWW-Authenticate challenges and Proxy-Authorization for Proxy-Authenticate challenges, selected by an explicit proxy flag, and return no header when it cannot satisfy any offered challenge. +- An auth handler MUST emit Authorization for WWW-Authenticate challenges and Proxy-Authorization for Proxy-Authenticate challenges, selected by an explicit proxy flag, and return no header when it cannot satisfy any offered challenge (AUTH-25). spec · `docs/product-spec/11-authentication.md:19` · high · sha:efba58233dd1 -- Static key-credential stamping MUST write the key into the configured header, default Authorization, and when a prefix is configured, prepend it followed by a single space, with the stamping step stateless after construction. +- Static key-credential stamping MUST write the key into the configured header, default Authorization, and when a prefix is configured, prepend it followed by a single space, with the stamping step stateless after construction (AUTH-26). spec · `docs/product-spec/11-authentication.md:19` · high · sha:efba58233dd1 -- There MUST be exactly one auth step occupying the single AUTH pillar stage, running nested inside both the redirect loop and the retry loop, so auth executes per redirect hop and per retry attempt, with redirect wrapping retry wrapping auth. +- There MUST be exactly one auth step occupying the single AUTH pillar stage, running nested inside both the redirect loop and the retry loop, so auth executes per redirect hop and per retry attempt, with redirect wrapping retry wrapping auth (AUTH-27). spec · `docs/product-spec/11-authentication.md:23` · high · sha:efba58233dd1 -- On any path where a credential will be attached, the auth step MUST reject a non-HTTPS request URL, case-insensitive, before any token fetch or header stamping, failing with an error naming the concrete step and the offending scheme; credentials MUST NOT be stamped over plaintext. +- On any path where a credential will be attached, the auth step MUST reject a non-HTTPS request URL, case-insensitive, before any token fetch or header stamping, failing with an error naming the concrete step and the offending scheme; credentials MUST NOT be stamped over plaintext (AUTH-28). spec · `docs/product-spec/11-authentication.md:23` · high · sha:efba58233dd1 -- On a cross-origin redirect re-issue, differing in scheme, host, or effective port under the RFC 6454 tuple and marked by the redirect step, the auth step MUST NOT stamp the caller's credential, MUST strip the internal cross-origin marker so it never reaches the wire, and MUST skip the HTTPS guard so a deliberately-allowed downgrade hop is forwarded credential-free rather than hard-failing; a same-origin re-issue MUST be re-stamped normally and remains subject to the HTTPS guard. +- On a cross-origin redirect re-issue, differing in scheme, host, or effective port under the RFC 6454 tuple and marked by the redirect step, the auth step MUST NOT stamp the caller's credential, MUST strip the internal cross-origin marker so it never reaches the wire, and MUST skip the HTTPS guard so a deliberately-allowed downgrade hop is forwarded credential-free rather than hard-failing; a same-origin re-issue MUST be re-stamped normally and remains subject to the HTTPS guard (AUTH-29). spec · `docs/product-spec/11-authentication.md:24` · high · sha:efba58233dd1 -- The cross-origin suppression mechanism MUST only be able to suppress credential stamping, never force a credential to be sent. +- The cross-origin suppression mechanism MUST only be able to suppress credential stamping, never force a credential to be sent (AUTH-29). spec · `docs/product-spec/11-authentication.md:24` · high · sha:efba58233dd1 -- On a 401 carrying a WWW-Authenticate header, the auth step MUST consult its challenge hook; if the hook yields a non-null replacement request, the step MUST close the original 401 and drive the replacement through a fresh copy of the downstream chain exactly once, with no further challenge handling on the replacement; the default hook yields no replacement. +- On a 401 carrying a WWW-Authenticate header, the auth step MUST consult its challenge hook; if the hook yields a non-null replacement request, the step MUST close the original 401 and drive the replacement through a fresh copy of the downstream chain exactly once, with no further challenge handling on the replacement; the default hook yields no replacement (AUTH-30). spec · `docs/product-spec/11-authentication.md:25` · high · sha:efba58233dd1 -- A 401 without a WWW-Authenticate header MUST be returned unchanged without consulting the challenge hook. +- A 401 without a WWW-Authenticate header MUST be returned unchanged without consulting the challenge hook (AUTH-33). spec · `docs/product-spec/11-authentication.md:25` · high · sha:efba58233dd1 -- If the challenge hook throws, or its async future completes exceptionally, or the async hook throws synchronously, the auth step MUST close the open 401 response body before propagating. +- If the challenge hook throws, or its async future completes exceptionally, or the async hook throws synchronously, the auth step MUST close the open 401 response body before propagating (AUTH-32). spec · `docs/product-spec/11-authentication.md:25` · high · sha:efba58233dd1 -- The 401 re-challenge replay MUST be gated on request-body replayability -- if the replacement carries a non-replayable body, the step MUST skip the replay, surface the original 401 unchanged, and MUST NOT close that original response, since the caller owns it. +- The 401 re-challenge replay MUST be gated on request-body replayability -- if the replacement carries a non-replayable body, the step MUST skip the replay, surface the original 401 unchanged, and MUST NOT close that original response, since the caller owns it (AUTH-31). spec · `docs/product-spec/11-authentication.md:26` · high · sha:efba58233dd1 -- The bearer auth step MUST stamp Authorization: Bearer using a token cached until a configurable refresh margin before expiry, default 30 seconds, ensuring concurrent requests racing on a missing/expiring token result in at most one provider fetch (single-flight) with a non-blocking hot-path read of a valid cached token. +- The bearer auth step MUST stamp Authorization: Bearer using a token cached until a configurable refresh margin before expiry, default 30 seconds, ensuring concurrent requests racing on a missing/expiring token result in at most one provider fetch (single-flight) with a non-blocking hot-path read of a valid cached token (AUTH-34). spec · `docs/product-spec/11-authentication.md:27` · high · sha:efba58233dd1 -- The bearer auth step MUST reject a null token and a token already expired at fetch time, evaluated with no margin, and MUST NOT cache a thrown provider error. +- The bearer auth step MUST reject a null token and a token already expired at fetch time, evaluated with no margin, and MUST NOT cache a thrown provider error (AUTH-35). spec · `docs/product-spec/11-authentication.md:27` · high · sha:efba58233dd1 -- On a 401 advertising a Bearer challenge, the bearer auth step MUST evict only the exact cached token that produced the 401, matched by the stamped header value, and re-stamp a single retry with a freshly fetched token, preserving a token another request already refreshed, surfacing the 401 unchanged when the rejected request carried no Authorization header or the response advertises no Bearer challenge, and firing the eviction-driven retry regardless of HTTP method. +- On a 401 advertising a Bearer challenge, the bearer auth step MUST evict only the exact cached token that produced the 401, matched by the stamped header value, and re-stamp a single retry with a freshly fetched token, preserving a token another request already refreshed, surfacing the 401 unchanged when the rejected request carried no Authorization header or the response advertises no Bearer challenge, and firing the eviction-driven retry regardless of HTTP method (AUTH-36). spec · `docs/product-spec/11-authentication.md:27` · high · sha:efba58233dd1 -- The async bearer step MUST implement a three-zone expiry policy without blocking the dispatching thread -- fresh tokens are stamped with no refresh, expiring-but-valid tokens are stamped immediately while an off-thread background refresh is kicked off, and expired/missing tokens await a fresh single-flight fetch, coalescing concurrent expiring/missing requests onto one fetch, not caching a failed fetch, and treating a failed background refresh as non-fatal since a valid token was already stamped. +- The async bearer step MUST implement a three-zone expiry policy without blocking the dispatching thread -- fresh tokens are stamped with no refresh, expiring-but-valid tokens are stamped immediately while an off-thread background refresh is kicked off, and expired/missing tokens await a fresh single-flight fetch, coalescing concurrent expiring/missing requests onto one fetch, not caching a failed fetch, and treating a failed background refresh as non-fatal since a valid token was already stamped (AUTH-37). spec · `docs/product-spec/11-authentication.md:27` · high · sha:efba58233dd1 -- In the async auth path, the HTTPS-guard failure and any challenge hook error SHOULD be delivered through the asynchronous channel, a failed future, rather than synchronously thrown. +- In the async auth path, the HTTPS-guard failure and any challenge hook error SHOULD be delivered through the asynchronous channel, a failed future, rather than synchronously thrown (AUTH-38). spec · `docs/product-spec/11-authentication.md:27` · high · sha:efba58233dd1 - The cryptographically-strong client nonce (at least 128 bits of entropy) must use `crypto.getRandomValues()` rather than `Math.random()`, since it must come from a CSPRNG, never a non-cryptographic RNG. design · `docs/sdk-design-nodejs/06-retry-redirect-and-authentication.md:68-70` · high · sha:b0e2bb42d809 @@ -97,9 +97,9 @@ ## Reference - Authentication has two largely independent halves -- a scheme-agnostic descriptor/resolver model that decides which auth alternative an operation requires, and a stamping/challenge half that puts credentials on the wire and reacts to server challenges. spec · `docs/product-spec/11-authentication.md:3` · high · sha:efba58233dd1 -- 401 eviction/refresh matching for bearer tokens is done on the stamped header string, not credential equality, so value equality is not required for the key credentials. +- 401 eviction/refresh matching for bearer tokens is done on the stamped header string, not credential equality, so value equality is not required for the key credentials (AUTH-36). spec · `docs/product-spec/11-authentication.md:8` · high · sha:efba58233dd1 -- The reference implementation enforces the 401 replayability gate on the synchronous auth step only; the async auth step does not currently apply a replayability gate and closes the original 401 before re-driving unconditionally, and a faithful port SHOULD apply the same gate on both paths. +- The reference implementation enforces the 401 replayability gate on the synchronous auth step only; the async auth step does not currently apply a replayability gate and closes the original 401 before re-driving unconditionally, and a faithful port SHOULD apply the same gate on both paths (AUTH-31). spec · `docs/product-spec/11-authentication.md:26` · high · sha:efba58233dd1 - An auth challenge is a parsed RFC 7235 WWW-Authenticate/Proxy-Authenticate directive, a scheme plus a parameter map, that a server returns on a 401/407 to indicate how a client may authenticate. spec · `docs/product-spec/appendix-a-glossary.md:7` · high · sha:f0b3d2058626 diff --git a/docs/knowledge/concurrency-and-async.md b/docs/knowledge/concurrency-and-async.md index 9c42c05..6b6de95 100644 --- a/docs/knowledge/concurrency-and-async.md +++ b/docs/knowledge/concurrency-and-async.md @@ -69,7 +69,7 @@ spec · `docs/product-spec/18-asynchronous-runtime-adapter-contract.md:33-33` · high · sha:f1bf00174456 - An adapter that owns an executor should shut it down gracefully on close — stopping new work and waiting for in-flight tasks rather than interrupting them — escalating to forceful shutdown only if the closing thread is itself interrupted, with callers needing eager abort using the interrupt/structured-cancellation path. spec · `docs/product-spec/18-asynchronous-runtime-adapter-contract.md:34-34` · high · sha:f1bf00174456 -- The async transport SPI should provide a no-op default close so lightweight/functional implementations need not implement lifecycle management, while any implementation that owns resources overrides it to follow the idempotent/ownership-aware/interrupt-safe close contract; behavior of executeAsync after close is undefined. +- The async transport SPI should provide a no-op default close so lightweight/functional implementations need not implement lifecycle management, while any implementation that owns resources overrides it to follow the idempotent/ownership-aware/interrupt-safe close contract; behavior of executeAsync after close is undefined (SEAM-15). spec · `docs/product-spec/18-asynchronous-runtime-adapter-contract.md:35-35` · high · sha:f1bf00174456 - Components documented as shared/reusable across concurrent requests (pipeline steps, auth handlers, redactors, factories) must be safe for concurrent invocation, with per-call mutable state kept on the call's local state and any shared mutable state synchronized. spec · `docs/product-spec/19-cross-cutting-invariants-and-policies.md:28` · high · sha:d6123be82c9e diff --git a/docs/knowledge/execution-context.md b/docs/knowledge/execution-context.md index 9519865..cc4ab7e 100644 --- a/docs/knowledge/execution-context.md +++ b/docs/knowledge/execution-context.md @@ -1,77 +1,77 @@ # execution-context ## Rules -- The execution context model MUST provide three context flavors forming a one-way promotion chain mirroring the call lifecycle -- a dispatch stage before any request, a request stage with an outgoing request assembled, and an exchange stage after a response arrives -- with promotion advancing dispatch to request to exchange only and the exchange stage terminal. +- The execution context model MUST provide three context flavors forming a one-way promotion chain mirroring the call lifecycle -- a dispatch stage before any request, a request stage with an outgoing request assembled, and an exchange stage after a response arrives -- with promotion advancing dispatch to request to exchange only and the exchange stage terminal (CTX-1). spec · `docs/product-spec/07-execution-context-model.md:7` · high · sha:5a9eacfb1c53 -- Each promotion in the execution context chain MUST be additive and non-mutating, producing a new instance without modifying the source, carrying forward the same instrumentation bundle and call key, and adding exactly one new artifact (the request when promoting dispatch to request, the response when promoting request to exchange). +- Each promotion in the execution context chain MUST be additive and non-mutating, producing a new instance without modifying the source, carrying forward the same instrumentation bundle and call key, and adding exactly one new artifact (the request when promoting dispatch to request, the response when promoting request to exchange) (CTX-2). spec · `docs/product-spec/07-execution-context-model.md:8` · high · sha:5a9eacfb1c53 -- The entire context promotion chain MUST share one call key -- a promotion carries the source's call key forward verbatim so all three flavors register under the identical store slot and successive promotions overwrite one entry. +- The entire context promotion chain MUST share one call key -- a promotion carries the source's call key forward verbatim so all three flavors register under the identical store slot and successive promotions overwrite one entry (CTX-3). spec · `docs/product-spec/07-execution-context-model.md:9` · high · sha:5a9eacfb1c53 -- A directly-constructed (off-chain) context without an explicit key MUST receive a fresh call-unique key using the same uniqueness guarantee as promoted contexts, and default construction MUST mint globally distinct keys across the whole process and all three flavors. +- A directly-constructed (off-chain) context without an explicit key MUST receive a fresh call-unique key using the same uniqueness guarantee as promoted contexts, and default construction MUST mint globally distinct keys across the whole process and all three flavors (CTX-5 / CTX-6). spec · `docs/product-spec/07-execution-context-model.md:14` · high · sha:5a9eacfb1c53 -- Because the call key participates in value-equality, two default-constructed contexts with otherwise identical fields are not equal; callers needing value-equality between contexts MUST be able to pin an explicit shared key. +- Because the call key participates in value-equality, two default-constructed contexts with otherwise identical fields are not equal; callers needing value-equality between contexts MUST be able to pin an explicit shared key (CTX-5). spec · `docs/product-spec/07-execution-context-model.md:14` · high · sha:5a9eacfb1c53 -- Registration MUST happen at promotion time, not at head-context construction -- constructing the initial dispatch context MUST NOT auto-register it, so a dispatch context never promoted leaves no store entry and its close is a harmless no-op. +- Registration MUST happen at promotion time, not at head-context construction -- constructing the initial dispatch context MUST NOT auto-register it, so a dispatch context never promoted leaves no store entry and its close is a harmless no-op (CTX-17). spec · `docs/product-spec/07-execution-context-model.md:15` · high · sha:5a9eacfb1c53 -- The context store MUST support an unconditional overwrite operation (install-or-replace, never throwing) used by promotion, and a reject-on-duplicate insert operation (install only if absent) that admits exactly one winner under concurrency and fails all others with an error naming the key. +- The context store MUST support an unconditional overwrite operation (install-or-replace, never throwing) used by promotion, and a reject-on-duplicate insert operation (install only if absent) that admits exactly one winner under concurrency and fails all others with an error naming the key (CTX-8). spec · `docs/product-spec/07-execution-context-model.md:20` · high · sha:5a9eacfb1c53 -- Closing a context MUST evict the store entry conditionally on reference identity, removing the slot only when the current occupant is the closing context (never by value equality), and removing a non-existent or already-replaced slot MUST be a well-defined no-op. +- Closing a context MUST evict the store entry conditionally on reference identity, removing the slot only when the current occupant is the closing context (never by value equality), and removing a non-existent or already-replaced slot MUST be a well-defined no-op (CTX-9). spec · `docs/product-spec/07-execution-context-model.md:21` · high · sha:5a9eacfb1c53 -- Only the context currently occupying the shared store slot (the furthest-reached link in the promotion chain) evicts on close; closing an intermediate link that was already promoted is a no-op. +- Only the context currently occupying the shared store slot (the furthest-reached link in the promotion chain) evicts on close; closing an intermediate link that was already promoted is a no-op (CTX-10). spec · `docs/product-spec/07-execution-context-model.md:21` · high · sha:5a9eacfb1c53 -- Looking up an unknown key MUST return an explicit absent result rather than throw, and removing an unknown or already-removed key MUST be a no-op, so double-close and cleanup-path closes are well-defined. +- Looking up an unknown key MUST return an explicit absent result rather than throw, and removing an unknown or already-removed key MUST be a no-op, so double-close and cleanup-path closes are well-defined (CTX-18). spec · `docs/product-spec/07-execution-context-model.md:22` · high · sha:5a9eacfb1c53 -- The cap-draining strategy SHOULD be a post-insert drain loop (drain until at or under the cap) rather than a single check-then-evict, so concurrent insert bursts converge to the bound instead of overshooting. +- The cap-draining strategy SHOULD be a post-insert drain loop (drain until at or under the cap) rather than a single check-then-evict, so concurrent insert bursts converge to the bound instead of overshooting (CTX-12). spec · `docs/product-spec/07-execution-context-model.md:27` · high · sha:5a9eacfb1c53 -- Each context MUST carry a correlation/instrumentation bundle exposing at minimum a trace id, a span id, trace flags, trace state, a trace-id encoding flavor, validity and remoteness flags, an active span, and a per-operation tracer factory, W3C Trace Context compatible for cross-service propagation. +- Each context MUST carry a correlation/instrumentation bundle exposing at minimum a trace id, a span id, trace flags, trace state, a trace-id encoding flavor, validity and remoteness flags, an active span, and a per-operation tracer factory, W3C Trace Context compatible for cross-service propagation (CTX-14). spec · `docs/product-spec/07-execution-context-model.md:31` · high · sha:5a9eacfb1c53 -- A disabled-tracing/no-op instrumentation bundle MUST be available as the default, with reserved invalid sentinels (all-zero trace id, all-zero span id, zero flags, empty state), isValid false, isRemote false, a no-op span, and a no-op tracer factory. +- A disabled-tracing/no-op instrumentation bundle MUST be available as the default, with reserved invalid sentinels (all-zero trace id, all-zero span id, zero flags, empty state), isValid false, isRemote false, a no-op span, and a no-op tracer factory (CTX-15). spec · `docs/product-spec/07-execution-context-model.md:31` · high · sha:5a9eacfb1c53 -- A context SHOULD carry an optional operation name (a schema-defined operation id, or absent), MUST carry it forward unchanged across every promotion, and MUST keep it advisory only, exposed to the tracing seam without influencing the request, dispatch decision, or store key. +- A context SHOULD carry an optional operation name (a schema-defined operation id, or absent), MUST carry it forward unchanged across every promotion, and MUST keep it advisory only, exposed to the tracing seam without influencing the request, dispatch decision, or store key (CTX-16). spec · `docs/product-spec/07-execution-context-model.md:32` · high · sha:5a9eacfb1c53 -- The per-operation tracer factory SHOULD default to a no-op emitting nothing so untraced call sites pay zero tracing cost, and its factory method MUST be safe to invoke concurrently. +- The per-operation tracer factory SHOULD default to a no-op emitting nothing so untraced call sites pay zero tracing cost, and its factory method MUST be safe to invoke concurrently (CTX-20). spec · `docs/product-spec/07-execution-context-model.md:32` · high · sha:5a9eacfb1c53 -- When folding thread-local diagnostic context into a log event, only allow-listed keys are folded; the default allow-list is exactly {trace.id, span.id}, a null (absent) allow-list folds every present key, and keys with null values are skipped, to prevent arbitrary application context from leaking into SDK-owned events. +- When folding thread-local diagnostic context into a log event, only allow-listed keys are folded; the default allow-list is exactly {trace.id, span.id}, a null (absent) allow-list folds every present key, and keys with null values are skipped, to prevent arbitrary application context from leaking into SDK-owned events (OBS-10). spec · `docs/product-spec/15-instrumentation-and-observability.md:20-20` · high · sha:1b678eca176d -- Adapters that move work/callbacks onto another thread should propagate the caller's diagnostic logging context across the hop (capture on the boundary thread, reinstate on the executing/callback thread) so post-hop log events retain correlation; this is an observability guarantee, not a functional one, so an adapter that omits it still executes exchanges correctly. +- Adapters that move work/callbacks onto another thread should propagate the caller's diagnostic logging context across the hop (capture on the boundary thread, reinstate on the executing/callback thread) so post-hop log events retain correlation; this is an observability guarantee, not a functional one, so an adapter that omits it still executes exchanges correctly (ASYNC-8). spec · `docs/product-spec/18-asynchronous-runtime-adapter-contract.md:20-20` · high · sha:f1bf00174456 -- When an adapter reinstates a captured context, it must first save the executing thread's prior context, install the captured context only for the work's duration, and restore the prior context afterward — including when the work throws — so a reused/pooled thread's own context is never clobbered. +- When an adapter reinstates a captured context, it must first save the executing thread's prior context, install the captured context only for the work's duration, and restore the prior context afterward — including when the work throws — so a reused/pooled thread's own context is never clobbered (ASYNC-9). spec · `docs/product-spec/18-asynchronous-runtime-adapter-contract.md:21-21` · high · sha:f1bf00174456 -- When an adapter propagates logging context, capture must occur at the point that identifies the logical caller — per-subscription for cold/reusable stream or promise objects, per-task-submission for executor decorators — not at object-construction time, so a reused async object picks up the live context of each use. +- When an adapter propagates logging context, capture must occur at the point that identifies the logical caller — per-subscription for cold/reusable stream or promise objects, per-task-submission for executor decorators — not at object-construction time, so a reused async object picks up the live context of each use (ASYNC-10). spec · `docs/product-spec/18-asynchronous-runtime-adapter-contract.md:22-22` · high · sha:f1bf00174456 -- When an adapter propagates logging context, capture and restore must be safe when no logging-context backend is installed: an absent context captures as empty, and reinstating an empty context clears the target thread's context rather than raising. +- When an adapter propagates logging context, capture and restore must be safe when no logging-context backend is installed: an absent context captures as empty, and reinstating an empty context clears the target thread's context rather than raising (ASYNC-11). spec · `docs/product-spec/18-asynchronous-runtime-adapter-contract.md:23-23` · high · sha:f1bf00174456 -- On runtimes where a newly created worker does not inherit the spawning thread's logging context (lightweight threads or plain thread-local contexts), an adapter that propagates logging context must explicitly transfer it at the thread-creation boundary, distinct from any carrier-hop guarantee the runtime provides. +- On runtimes where a newly created worker does not inherit the spawning thread's logging context (lightweight threads or plain thread-local contexts), an adapter that propagates logging context must explicitly transfer it at the thread-creation boundary, distinct from any carrier-hop guarantee the runtime provides (ASYNC-12). spec · `docs/product-spec/18-asynchronous-runtime-adapter-contract.md:24-24` · high · sha:f1bf00174456 ## Constraints -- Each call's store key MUST be unique per call and MUST NOT be derived from the trace identifier, or the trace+span pair, alone, so two concurrent calls sharing a trace id or even a span id receive distinct keys and never evict each other. +- Each call's store key MUST be unique per call and MUST NOT be derived from the trace identifier, or the trace+span pair, alone, so two concurrent calls sharing a trace id or even a span id receive distinct keys and never evict each other (CTX-4). spec · `docs/product-spec/07-execution-context-model.md:13` · high · sha:5a9eacfb1c53 -- Contexts MUST be immutable and shareable without external synchronization, and the store MUST be thread-safe such that contexts with distinct call keys can be registered, overwritten, and removed concurrently without external locking. +- Contexts MUST be immutable and shareable without external synchronization, and the store MUST be thread-safe such that contexts with distinct call keys can be registered, overwritten, and removed concurrently without external locking (CTX-7). spec · `docs/product-spec/07-execution-context-model.md:19` · high · sha:5a9eacfb1c53 -- The context store MUST be bounded, enforcing a maximum number of tracked entries and draining back to at or below that cap after each insert, as a backstop so a caller who fails to close a context on an exception path leaks at most the cap's worth of entries. +- The context store MUST be bounded, enforcing a maximum number of tracked entries and draining back to at or below that cap after each insert, as a backstop so a caller who fails to close a context on an exception path leaks at most the cap's worth of entries (CTX-11). spec · `docs/product-spec/07-execution-context-model.md:26` · high · sha:5a9eacfb1c53 -- The context store MUST keep the pinned request/response graph reachable while a context remains stored; reimplementations MUST NOT hold contexts by weak or soft references, and MUST treat the bounded cap, not garbage collection, as the leak backstop. +- The context store MUST keep the pinned request/response graph reachable while a context remains stored; reimplementations MUST NOT hold contexts by weak or soft references, and MUST treat the bounded cap, not garbage collection, as the leak backstop (CTX-19). spec · `docs/product-spec/07-execution-context-model.md:26` · high · sha:5a9eacfb1c53 -- Eviction victim selection in the context store is arbitrary -- the store provides no ordering and no guarantee that any particular entry, including the just-inserted one, survives an insert that trips the cap, and a port MUST NOT rely on any specific entry surviving. +- Eviction victim selection in the context store is arbitrary -- the store provides no ordering and no guarantee that any particular entry, including the just-inserted one, survives an insert that trips the cap, and a port MUST NOT rely on any specific entry surviving (CTX-13). spec · `docs/product-spec/07-execution-context-model.md:27` · high · sha:5a9eacfb1c53 -- Because the default no-op instrumentation bundle shares constant identifiers across every untraced call, call-key derivation MUST remain call-unique even when every bundle field is identical. +- Because the default no-op instrumentation bundle shares constant identifiers across every untraced call, call-key derivation MUST remain call-unique even when every bundle field is identical (CTX-4). spec · `docs/product-spec/07-execution-context-model.md:31` · high · sha:5a9eacfb1c53 ## Conclusions -- Trace ids cannot be used alone for call-key derivation because a disabled-tracing context shares one constant trace id across every untraced call, an inbound distributed trace shares one trace id across many spans, and a tracer may reuse a span id. +- Trace ids cannot be used alone for call-key derivation because a disabled-tracing context shares one constant trace id across every untraced call, an inbound distributed trace shares one trace id across many spans, and a tracer may reuse a span id (CTX-4). spec · `docs/product-spec/07-execution-context-model.md:13` · high · sha:5a9eacfb1c53 -- Value-equality removal is rejected for context store eviction because contexts are value-equal, so a value-equality remove could let a stale context evict a structurally-identical live sibling. +- Value-equality removal is rejected for context store eviction because contexts are value-equal, so a value-equality remove could let a stale context evict a structurally-identical live sibling (CTX-9). spec · `docs/product-spec/07-execution-context-model.md:21` · high · sha:5a9eacfb1c53 -- The context store is bounded rather than left unbounded because a registered context strongly pins the full request-and-response graph, including a possibly-unread body holding a connection. +- The context store is bounded rather than left unbounded because a registered context strongly pins the full request-and-response graph, including a possibly-unread body holding a connection (CTX-11 / CTX-19). spec · `docs/product-spec/07-execution-context-model.md:26` · high · sha:5a9eacfb1c53 ## Reference - A single in-flight call's correlation state is modeled as a one-way promotion chain of three immutable context flavors, each carrying a shared instrumentation bundle and a single call-unique key, registered in a bounded process-wide store keyed by that call key. spec · `docs/product-spec/07-execution-context-model.md:3` · high · sha:5a9eacfb1c53 -- The operation name is introduced at the request stage as an argument to the dispatch-to-request promotion. +- The operation name is introduced at the request stage as an argument to the dispatch-to-request promotion (CTX-2). spec · `docs/product-spec/07-execution-context-model.md:8` · high · sha:5a9eacfb1c53 -- The reference implementation's default call key appends a process-wide monotonic counter to a traceId:spanId rendering. +- The reference implementation's default call key appends a process-wide monotonic counter to a traceId:spanId rendering (CTX-4). spec · `docs/product-spec/07-execution-context-model.md:13` · high · sha:5a9eacfb1c53 ## Conflicts diff --git a/docs/knowledge/message-bodies.md b/docs/knowledge/message-bodies.md index e2b0fa3..d88c88d 100644 --- a/docs/knowledge/message-bodies.md +++ b/docs/knowledge/message-bodies.md @@ -21,7 +21,7 @@ spec · `docs/product-spec/06-request-and-response-body-lifecycle.md:8-8` · high · sha:c2bf15dc8a06 - A materialize-once operation MUST return the same body unchanged when already replayable, and otherwise drain the body's write output exactly once into an in-memory buffer and return a replayable buffer-backed body, after which the original MUST be treated as consumed (BODY-3 / HTTP-37). spec · `docs/product-spec/06-request-and-response-body-lifecycle.md:9-9` · high · sha:c2bf15dc8a06 -- A single-use body MUST fail loudly on a second write, never silently emitting zero bytes, and the consume-once guard MUST be race-safe so that under concurrent writes at most one proceeds and the losers observe a clear error (BODY-3 / HTTP-37). +- A single-use body MUST fail loudly on a second write, never silently emitting zero bytes, and the consume-once guard MUST be race-safe so that under concurrent writes at most one proceeds and the losers observe a clear error (BODY-3 / HTTP-37 / BODY-6 / BODY-7). spec · `docs/product-spec/06-request-and-response-body-lifecycle.md:9-9` · high · sha:c2bf15dc8a06 - A single-use body that owns a closeable source MUST release that source as part of its single write, so skipping materialization does not leak it (BODY-8). spec · `docs/product-spec/06-request-and-response-body-lifecycle.md:10-10` · high · sha:c2bf15dc8a06 @@ -89,7 +89,7 @@ design · `docs/sdk-design-nodejs/06-retry-redirect-and-authentication.md:38-42` · high · sha:b0e2bb42d809 ## Reference -- The reference implementation of the consume-once guard for a single-use body is an atomic compare-and-set (BODY-3 / HTTP-37). +- The reference implementation of the consume-once guard for a single-use body is an atomic compare-and-set (BODY-3 / HTTP-37 / BODY-7). spec · `docs/product-spec/06-request-and-response-body-lifecycle.md:9-9` · high · sha:c2bf15dc8a06 - In the reference implementation, the buffered-source-backed single-use body drains and closes its source during write, while the raw byte-stream-backed bodies do not close their stream during write — the rewindable variant keeps it open to replay and the one-shot variant leaves the caller-supplied stream unclosed per its documented ownership (BODY-8). spec · `docs/product-spec/06-request-and-response-body-lifecycle.md:10-10` · high · sha:c2bf15dc8a06 diff --git a/docs/knowledge/pagination.md b/docs/knowledge/pagination.md index 880bd9d..d954388 100644 --- a/docs/knowledge/pagination.md +++ b/docs/knowledge/pagination.md @@ -17,9 +17,9 @@ spec · `docs/product-spec/12-pagination.md:19-19` · high · sha:ba759edd34ec - A Page MUST be a closeable resource owning exactly one underlying response, whoever pulls a page owns closing it, closing the page MUST release that response's body/connection, and a component that hands a caller a live page MUST NOT itself close the response. spec · `docs/product-spec/12-pagination.md:20-20` · high · sha:ba759edd34ec -- A pagination strategy's parse output MUST carry items plus a next-request value where a null/absent next-request is the single exclusive end-of-stream signal, parse MUST always return a well-formed non-null result, termination MUST never be signaled by throwing or a side channel, and an empty items list with a non-null next-request is a valid non-terminal page. +- A pagination strategy's parse output MUST carry items plus a next-request value where a null/absent next-request is the single exclusive end-of-stream signal, parse MUST always return a well-formed non-null result, termination MUST never be signaled by throwing or a side channel, and an empty items list with a non-null next-request is a valid non-terminal page (PAGE-4). spec · `docs/product-spec/12-pagination.md:26-26` · high · sha:ba759edd34ec -- A pagination strategy MUST read everything it needs from the response synchronously inside parse since the body is single-use, MUST NOT retain the response or its body beyond the call, MUST NOT close or mutate the response, and strategies MUST be immutable and safe to share concurrently. +- A pagination strategy MUST read everything it needs from the response synchronously inside parse since the body is single-use, MUST NOT retain the response or its body beyond the call, MUST NOT close or mutate the response, and strategies MUST be immutable and safe to share concurrently (PAGE-5). spec · `docs/product-spec/12-pagination.md:27-27` · high · sha:ba759edd34ec - The pagination engine MUST accept a page cap bounding a server that never advances its cursor, the cap counts exchanges/pages not items, the engine MUST stop fetching once the cap is reached even if the strategy reports a next-request, and the cap MUST be validated as strictly positive at construction rather than lazily. spec · `docs/product-spec/12-pagination.md:31-31` · high · sha:ba759edd34ec diff --git a/docs/knowledge/pipeline.md b/docs/knowledge/pipeline.md index d4f58d6..a92c441 100644 --- a/docs/knowledge/pipeline.md +++ b/docs/knowledge/pipeline.md @@ -1,115 +1,115 @@ # pipeline ## Rules -- Steps MUST execute in a single fixed total order derived from stage assignment -- a step in a lower-ordered stage runs before (wraps) a step in a higher-ordered stage on the inbound path and observes the response later on the outbound path, and this cross-stage order is deterministic and independent of insertion order. +- Steps MUST execute in a single fixed total order derived from stage assignment -- a step in a lower-ordered stage runs before (wraps) a step in a higher-ordered stage on the inbound path and observes the response later on the outbound path, and this cross-stage order is deterministic and independent of insertion order (PIPE-1). spec · `docs/product-spec/08-execution-pipelines.md:9` · high · sha:33e9443472ce -- The runtime MUST preserve the pillar precedence chain REDIRECT to RETRY to AUTH to LOGGING to SERDE (outer to inner), plus an outermost pre-redirect slot outside both loops and a terminal SEND hop innermost, and a step's placement relative to these boundaries determines whether it sees per-hop/per-attempt responses or only the single terminal response. +- The runtime MUST preserve the pillar precedence chain REDIRECT to RETRY to AUTH to LOGGING to SERDE (outer to inner), plus an outermost pre-redirect slot outside both loops and a terminal SEND hop innermost, and a step's placement relative to these boundaries determines whether it sees per-hop/per-attempt responses or only the single terminal response (PIPE-2). spec · `docs/product-spec/08-execution-pipelines.md:10` · high · sha:33e9443472ce -- The stage list SHOULD interleave user-extensible slots around each pillar (a pre and post slot) and SHOULD use sparse numeric order keys so new stages can be inserted without renumbering. +- The stage list SHOULD interleave user-extensible slots around each pillar (a pre and post slot) and SHOULD use sparse numeric order keys so new stages can be inserted without renumbering (PIPE-3). spec · `docs/product-spec/08-execution-pipelines.md:11` · high · sha:33e9443472ce -- Installing a distinct second step onto an occupied pillar stage, via any add or a bulk reload, MUST fail fast naming both step types and pointing at the replace path, rather than silently overwriting. +- Installing a distinct second step onto an occupied pillar stage, via any add or a bulk reload, MUST fail fast naming both step types and pointing at the replace path, rather than silently overwriting (PIPE-5). spec · `docs/product-spec/08-execution-pipelines.md:12` · high · sha:33e9443472ce -- Re-installing the same step onto its pillar stage MUST be idempotent, distinguished by reference identity, not value equality. +- Re-installing the same step onto its pillar stage MUST be idempotent, distinguished by reference identity, not value equality (PIPE-6). spec · `docs/product-spec/08-execution-pipelines.md:12` · high · sha:33e9443472ce -- An empty pipeline MUST dispatch directly to the terminal transport, threading the caller's per-call options, and SHOULD do so without allocating per-call cursor state. +- An empty pipeline MUST dispatch directly to the terminal transport, threading the caller's per-call options, and SHOULD do so without allocating per-call cursor state (PIPE-9). spec · `docs/product-spec/08-execution-pipelines.md:16` · high · sha:33e9443472ce -- The built runtime MUST be immutable after construction, and each send MUST allocate its own per-call cursor so concurrent calls share no mutable pipeline state. +- The built runtime MUST be immutable after construction, and each send MUST allocate its own per-call cursor so concurrent calls share no mutable pipeline state (PIPE-10). spec · `docs/product-spec/08-execution-pipelines.md:16` · high · sha:33e9443472ce -- Steps MUST be safe for concurrent invocation, with per-request mutable state living in the per-call cursor, never on the step. +- Steps MUST be safe for concurrent invocation, with per-request mutable state living in the per-call cursor, never on the step (PIPE-11). spec · `docs/product-spec/08-execution-pipelines.md:16` · high · sha:33e9443472ce -- Each step MUST be bidirectional -- it receives the inbound request, may invoke the rest of the chain, may inspect or substitute the outbound response, and may short-circuit by returning a synthetic response without invoking the chain. +- Each step MUST be bidirectional -- it receives the inbound request, may invoke the rest of the chain, may inspect or substitute the outbound response, and may short-circuit by returning a synthetic response without invoking the chain (PIPE-12). spec · `docs/product-spec/08-execution-pipelines.md:17` · high · sha:33e9443472ce -- Invoking the next step MUST advance a monotonic cursor and invoke it; when exhausted it MUST dispatch the current in-flight request to the terminal transport, threading the caller's per-call options, and the cursor MUST only move forward within a single un-forked drive. +- Invoking the next step MUST advance a monotonic cursor and invoke it; when exhausted it MUST dispatch the current in-flight request to the terminal transport, threading the caller's per-call options, and the cursor MUST only move forward within a single un-forked drive (PIPE-13). spec · `docs/product-spec/08-execution-pipelines.md:17` · high · sha:33e9443472ce -- A substituted request MUST propagate to every downstream step and the terminal dispatch. +- A substituted request MUST propagate to every downstream step and the terminal dispatch (PIPE-14). spec · `docs/product-spec/08-execution-pipelines.md:17` · high · sha:33e9443472ce -- A step that drives the downstream chain more than once (retry re-attempting, redirect following a hop, auth retrying after a challenge) MUST fork a fresh cursor for each re-drive rather than reusing the same next handle; reusing the handle resumes past already-visited steps and MUST be treated as a defect. +- A step that drives the downstream chain more than once (retry re-attempting, redirect following a hop, auth retrying after a challenge) MUST fork a fresh cursor for each re-drive rather than reusing the same next handle; reusing the handle resumes past already-visited steps and MUST be treated as a defect (PIPE-15). spec · `docs/product-spec/08-execution-pipelines.md:18` · high · sha:33e9443472ce -- A port MUST provide an equivalent cursor-fork primitive and its wrapping pillar steps MUST use it. +- A port MUST provide an equivalent cursor-fork primitive and its wrapping pillar steps MUST use it (PIPE-15). spec · `docs/product-spec/08-execution-pipelines.md:18` · high · sha:33e9443472ce -- A forked cursor MUST resume from the same position as its parent, carry the current in-flight request, and share the immutable options, with forks advancing independently. +- A forked cursor MUST resume from the same position as its parent, carry the current in-flight request, and share the immutable options, with forks advancing independently (PIPE-16). spec · `docs/product-spec/08-execution-pipelines.md:18` · high · sha:33e9443472ce -- The caller's per-call options MUST be carried unchanged for the entire call, including across every re-drive fork, readable by any step, and threaded into the terminal dispatch; options MUST be immutable/shared, not copied-and-diverged per fork. +- The caller's per-call options MUST be carried unchanged for the entire call, including across every re-drive fork, readable by any step, and threaded into the terminal dispatch; options MUST be immutable/shared, not copied-and-diverged per fork (PIPE-17). spec · `docs/product-spec/08-execution-pipelines.md:19` · high · sha:33e9443472ce -- A wrapping step that re-drives the chain MUST release each superseded intermediate response, closing its body before the next drive, and MUST NOT close the response it ultimately hands back to the caller, so close-responsibility passes outward. +- A wrapping step that re-drives the chain MUST release each superseded intermediate response, closing its body before the next drive, and MUST NOT close the response it ultimately hands back to the caller, so close-responsibility passes outward (PIPE-40). spec · `docs/product-spec/08-execution-pipelines.md:20` · high · sha:33e9443472ce -- On paths that abandon a re-drive (redirect cycle, non-replayable body, budget exhausted), the in-flight response MUST be returned unclosed. +- On paths that abandon a re-drive (redirect cycle, non-replayable body, budget exhausted), the in-flight response MUST be returned unclosed (PIPE-40). spec · `docs/product-spec/08-execution-pipelines.md:20` · high · sha:33e9443472ce -- Non-pillar stages MUST hold an ordered sequence where append adds to the tail and prepend to the head, preserving relative order through build and any re-bucketing edit. +- Non-pillar stages MUST hold an ordered sequence where append adds to the tail and prepend to the head, preserving relative order through build and any re-bucketing edit (PIPE-7). spec · `docs/product-spec/08-execution-pipelines.md:24` · high · sha:33e9443472ce -- The surgical insert-after/insert-before and replace edits MUST act relative to the first existing instance of an anchor type, and the inserted/replacing step MUST declare the same stage as the anchor; a cross-stage insert/replace MUST be rejected. +- The surgical insert-after/insert-before and replace edits MUST act relative to the first existing instance of an anchor type, and the inserted/replacing step MUST declare the same stage as the anchor; a cross-stage insert/replace MUST be rejected (PIPE-18 / PIPE-19). spec · `docs/product-spec/08-execution-pipelines.md:24` · high · sha:33e9443472ce -- Remove MUST delete every instance of a step type, preserving relative order, and be a no-op when the type is absent. +- Remove MUST delete every instance of a step type, preserving relative order, and be a no-op when the type is absent (PIPE-20). spec · `docs/product-spec/08-execution-pipelines.md:24` · high · sha:33e9443472ce -- An insert-relative or replace edit whose anchor type is absent MUST fail identifying the missing type. +- An insert-relative or replace edit whose anchor type is absent MUST fail identifying the missing type (PIPE-21). spec · `docs/product-spec/08-execution-pipelines.md:24` · high · sha:33e9443472ce -- Every mutation that re-buckets steps by stage MUST re-derive the flattened order deterministically, so the observable ordering after an edit equals building the same set from scratch. +- Every mutation that re-buckets steps by stage MUST re-derive the flattened order deterministically, so the observable ordering after an edit equals building the same set from scratch (PIPE-22). spec · `docs/product-spec/08-execution-pipelines.md:25` · high · sha:33e9443472ce -- A bulk reload MUST be all-or-nothing -- a pillar collision leaves the existing collection completely unchanged rather than a partial rebuild. +- A bulk reload MUST be all-or-nothing -- a pillar collision leaves the existing collection completely unchanged rather than a partial rebuild (PIPE-23). spec · `docs/product-spec/08-execution-pipelines.md:25` · high · sha:33e9443472ce -- The standard-resilience preset MUST install into empty pillar slots only, validating up front that no target pillar is occupied and rejecting the whole call, installing nothing, if any pillar is occupied. +- The standard-resilience preset MUST install into empty pillar slots only, validating up front that no target pillar is occupied and rejecting the whole call, installing nothing, if any pillar is occupied (PIPE-24). spec · `docs/product-spec/08-execution-pipelines.md:25` · high · sha:33e9443472ce -- build() MUST produce the ordered sequence by flattening stages in declaration order, skipping SEND, into an immutable runtime that exposes a read-only, ordered view of its steps. +- build() MUST produce the ordered sequence by flattening stages in declaration order, skipping SEND, into an immutable runtime that exposes a read-only, ordered view of its steps (PIPE-25). spec · `docs/product-spec/08-execution-pipelines.md:26` · high · sha:33e9443472ce -- The shipped pillar families SHOULD lock their stage assignment so a subclass cannot relocate out of its pillar. +- The shipped pillar families SHOULD lock their stage assignment so a subclass cannot relocate out of its pillar (PIPE-36). spec · `docs/product-spec/08-execution-pipelines.md:26` · high · sha:33e9443472ce -- A step whose correctness depends on the single terminal response, such as status-to-typed-error mapping, MUST occupy the outermost pre-redirect slot so it runs outside both loops, and on a non-error status MUST return the response untouched. +- A step whose correctness depends on the single terminal response, such as status-to-typed-error mapping, MUST occupy the outermost pre-redirect slot so it runs outside both loops, and on a non-error status MUST return the response untouched (PIPE-37). spec · `docs/product-spec/08-execution-pipelines.md:26` · high · sha:33e9443472ce -- The runtime MUST itself implement the transport SPI, delegating execute/execute-async to its own send/send-async (with and without options), so a configured pipeline can stand in wherever a transport is expected and options survive the indirection. +- The runtime MUST itself implement the transport SPI, delegating execute/execute-async to its own send/send-async (with and without options), so a configured pipeline can stand in wherever a transport is expected and options survive the indirection (PIPE-26). spec · `docs/product-spec/08-execution-pipelines.md:30` · high · sha:33e9443472ce -- Closing the pipeline MUST be a no-op with respect to the underlying transport -- the pipeline never owns its transport and MUST NOT close it. +- Closing the pipeline MUST be a no-op with respect to the underlying transport -- the pipeline never owns its transport and MUST NOT close it (PIPE-27). spec · `docs/product-spec/08-execution-pipelines.md:30` · high · sha:33e9443472ce -- The runtime SHOULD offer convenience constructors for a step-less pipeline forwarding directly to a transport and a standard pipeline installing the default resilience pillars, sync being redirect+retry+instrumentation and async being retry+instrumentation with a caller-supplied scheduler for non-blocking backoff. +- The runtime SHOULD offer convenience constructors for a step-less pipeline forwarding directly to a transport and a standard pipeline installing the default resilience pillars, sync being redirect+retry+instrumentation and async being retry+instrumentation with a caller-supplied scheduler for non-blocking backoff (PIPE-39). spec · `docs/product-spec/08-execution-pipelines.md:31` · high · sha:33e9443472ce -- The async runtime MUST reuse the identical stage identities and staging policy as the sync runtime; the two MUST NOT each re-derive ordering independently. +- The async runtime MUST reuse the identical stage identities and staging policy as the sync runtime; the two MUST NOT each re-derive ordering independently (PIPE-28). spec · `docs/product-spec/08-execution-pipelines.md:35` · high · sha:33e9443472ce -- An async step MUST NOT throw synchronously to signal a transport/async failure -- it MUST return a future completing exceptionally -- and MAY throw synchronously only for caller-bug argument validation. +- An async step MUST NOT throw synchronously to signal a transport/async failure -- it MUST return a future completing exceptionally -- and MAY throw synchronously only for caller-bug argument validation (PIPE-29). spec · `docs/product-spec/08-execution-pipelines.md:35` · high · sha:33e9443472ce -- The async runtime MUST defensively normalize any synchronous exception from a step's async entry point, or the empty-pipeline dispatch, into an exceptionally-completed future, while fatal/unrecoverable errors propagate synchronously and MUST NOT be swallowed. +- The async runtime MUST defensively normalize any synchronous exception from a step's async entry point, or the empty-pipeline dispatch, into an exceptionally-completed future, while fatal/unrecoverable errors propagate synchronously and MUST NOT be swallowed (PIPE-30). spec · `docs/product-spec/08-execution-pipelines.md:35` · high · sha:33e9443472ce -- The async terminal response-mapping operator MUST, on success, apply the handler then close the response, tolerating idempotent double-close; on failure it MUST unwrap async-wrapper exceptions to the original cause and MUST close any response accompanying a failure to avoid leaking the body. +- The async terminal response-mapping operator MUST, on success, apply the handler then close the response, tolerating idempotent double-close; on failure it MUST unwrap async-wrapper exceptions to the original cause and MUST close any response accompanying a failure to avoid leaking the body (PIPE-31). spec · `docs/product-spec/08-execution-pipelines.md:36` · high · sha:33e9443472ce -- The sync-to-async bridge MUST require a caller-supplied executor with no default, run the wrapped synchronous pipeline as a single opaque unit on that executor so its steps stay synchronous on the worker and do not gain per-step concurrency, and thread per-call options into the wrapped send. +- The sync-to-async bridge MUST require a caller-supplied executor with no default, run the wrapped synchronous pipeline as a single opaque unit on that executor so its steps stay synchronous on the worker and do not gain per-step concurrency, and thread per-call options into the wrapped send (PIPE-33). spec · `docs/product-spec/08-execution-pipelines.md:40` · high · sha:33e9443472ce -- Cancelling the sync-to-async bridge's future with interruption MUST interrupt the worker running the in-flight send, and cancelling without interruption MUST complete as cancelled without interrupting. +- Cancelling the sync-to-async bridge's future with interruption MUST interrupt the worker running the in-flight send, and cancelling without interruption MUST complete as cancelled without interrupting (PIPE-33). spec · `docs/product-spec/08-execution-pipelines.md:40` · high · sha:33e9443472ce -- The async-to-sync bridge MUST block on the async result per call while preserving options and MUST honor thread interruption -- on interrupt it restores the flag, cancels the in-flight future, and surfaces an interrupted-I/O error. +- The async-to-sync bridge MUST block on the async result per call while preserving options and MUST honor thread interruption -- on interrupt it restores the flag, cancels the in-flight future, and surfaces an interrupted-I/O error (PIPE-34). spec · `docs/product-spec/08-execution-pipelines.md:40` · high · sha:33e9443472ce -- The builder SHOULD provide two unambiguous ways to seed from an existing pipeline -- FLATTEN, which copies its steps and transport so they run in the same loops, versus NEST, which treats it as an opaque transport so the new steps run once outside the nested loops -- and a port MUST make the flatten-vs-nest choice explicit rather than accidental. +- The builder SHOULD provide two unambiguous ways to seed from an existing pipeline -- FLATTEN, which copies its steps and transport so they run in the same loops, versus NEST, which treats it as an opaque transport so the new steps run once outside the nested loops -- and a port MUST make the flatten-vs-nest choice explicit rather than accidental (PIPE-35). spec · `docs/product-spec/08-execution-pipelines.md:41` · high · sha:33e9443472ce -- The response-side outcome MUST be a closed sum type with exactly two variants -- a success carrying a response and a failure carrying a throwable -- mutually exclusive and jointly exhaustive, with derivable accessors and a fold that applies exactly one of two branches at most once per call. +- The response-side outcome MUST be a closed sum type with exactly two variants -- a success carrying a response and a failure carrying a throwable -- mutually exclusive and jointly exhaustive, with derivable accessors and a fold that applies exactly one of two branches at most once per call (RECOV-1). spec · `docs/product-spec/08-execution-pipelines.md:45` · high · sha:33e9443472ce -- The unified orchestrator MUST catch every throwable from any request-chain step and from the transport invocation, convert it into a Failure, and thread it through the response recovery chain; no throwable from the pre-request phase or the transport may bypass the recovery hooks. +- The unified orchestrator MUST catch every throwable from any request-chain step and from the transport invocation, convert it into a Failure, and thread it through the response recovery chain; no throwable from the pre-request phase or the transport may bypass the recovery hooks (RECOV-2). spec · `docs/product-spec/08-execution-pipelines.md:46` · high · sha:33e9443472ce -- The request recovery chain MUST apply its ordered steps as a sequential left-to-right fold where the output of step N is the input of step N+1; an empty chain returns the input unchanged, and a throwing step aborts the remainder and propagates. +- The request recovery chain MUST apply its ordered steps as a sequential left-to-right fold where the output of step N is the input of step N+1; an empty chain returns the input unchanged, and a throwing step aborts the remainder and propagates (RECOV-3). spec · `docs/product-spec/08-execution-pipelines.md:47` · high · sha:33e9443472ce -- Response steps (response-to-response) MUST run only when the current outcome is a Success; on a Failure the entire response-step phase is skipped. +- Response steps (response-to-response) MUST run only when the current outcome is a Success; on a Failure the entire response-step phase is skipped (RECOV-4). spec · `docs/product-spec/08-execution-pipelines.md:48` · high · sha:33e9443472ce -- Recovery steps MUST be applied to every outcome, successes and failures, sequentially and always, observing the terminal outcome including a failure a response step just produced by throwing. +- Recovery steps MUST be applied to every outcome, successes and failures, sequentially and always, observing the terminal outcome including a failure a response step just produced by throwing (RECOV-5). spec · `docs/product-spec/08-execution-pipelines.md:48` · high · sha:33e9443472ce -- The fold order MUST be all response steps first (on the success path), then all recovery steps, in declared order within each group. +- The fold order MUST be all response steps first (on the success path), then all recovery steps, in declared order within each group (RECOV-6). spec · `docs/product-spec/08-execution-pipelines.md:48` · high · sha:33e9443472ce -- If a response step throws, its throwable MUST be converted into a Failure fed to the subsequent recovery steps, never propagated out of the response chain, so error-mapping steps flow through recovery exactly like a transport error. +- If a response step throws, its throwable MUST be converted into a Failure fed to the subsequent recovery steps, never propagated out of the response chain, so error-mapping steps flow through recovery exactly like a transport error (RECOV-7). spec · `docs/product-spec/08-execution-pipelines.md:49` · high · sha:33e9443472ce -- If a recovery step throws, its throwable MUST be wrapped into a Failure fed to the next recovery step, never aborting the remaining recovery steps, and the chain's apply operation MUST NOT throw under any input. +- If a recovery step throws, its throwable MUST be wrapped into a Failure fed to the next recovery step, never aborting the remaining recovery steps, and the chain's apply operation MUST NOT throw under any input (RECOV-8). spec · `docs/product-spec/08-execution-pipelines.md:49` · high · sha:33e9443472ce -- Recovery steps SHOULD surface errors by returning a Failure rather than throwing. +- Recovery steps SHOULD surface errors by returning a Failure rather than throwing (RECOV-9). spec · `docs/product-spec/08-execution-pipelines.md:49` · high · sha:33e9443472ce -- The orchestrator's dispatch MUST unwrap the final outcome by returning the contained response on Success, or rethrowing the contained throwable unchanged on Failure with no wrapping or substitution; any typed-exception surfacing must be done by a recovery step constructing the error and returning a Failure. +- The orchestrator's dispatch MUST unwrap the final outcome by returning the contained response on Success, or rethrowing the contained throwable unchanged on Failure with no wrapping or substitution; any typed-exception surfacing must be done by a recovery step constructing the error and returning a Failure (RECOV-10). spec · `docs/product-spec/08-execution-pipelines.md:50` · high · sha:33e9443472ce -- When wrapping a cancellation/interruption throwable into a Failure, the wrapping helper MUST re-assert the cancellation signal on the current context before returning, so code later blocked on the outcome still observes the cancellation. +- When wrapping a cancellation/interruption throwable into a Failure, the wrapping helper MUST re-assert the cancellation signal on the current context before returning, so code later blocked on the outcome still observes the cancellation (RECOV-11). spec · `docs/product-spec/08-execution-pipelines.md:50` · high · sha:33e9443472ce -- When a response or recovery step throws while holding a Success response, the pipeline MUST close/release that in-hand response before wrapping the throwable, attaching any close error as suppressed so it never masks the primary, releasing the response exactly once. +- When a response or recovery step throws while holding a Success response, the pipeline MUST close/release that in-hand response before wrapping the throwable, attaching any close error as suppressed so it never masks the primary, releasing the response exactly once (RECOV-12). spec · `docs/product-spec/08-execution-pipelines.md:51` · high · sha:33e9443472ce -- When a step handed a Success deliberately returns a different outcome, whether a Success-to-Failure transform or a substitute Success, the pipeline MUST NOT auto-close the discarded original response; the transforming step owns releasing the response it drops. +- When a step handed a Success deliberately returns a different outcome, whether a Success-to-Failure transform or a substitute Success, the pipeline MUST NOT auto-close the discarded original response; the transforming step owns releasing the response it drops (RECOV-13). spec · `docs/product-spec/08-execution-pipelines.md:51` · high · sha:33e9443472ce -- A chain's step lists MUST behave as immutable after construction, and the response recovery chain MUST defensively copy both its lists at construction. +- A chain's step lists MUST behave as immutable after construction, and the response recovery chain MUST defensively copy both its lists at construction (RECOV-14). spec · `docs/product-spec/08-execution-pipelines.md:52` · high · sha:33e9443472ce -- Recovery chain steps MUST be safe for concurrent invocation, with per-request state in the passed context or the value being transformed, never on the step. +- Recovery chain steps MUST be safe for concurrent invocation, with per-request state in the passed context or the value being transformed, never on the step (RECOV-14). spec · `docs/product-spec/08-execution-pipelines.md:52` · high · sha:33e9443472ce -- The status-to-typed-exception mapping step MUST treat only 400..599 as errors, mapping to the matching typed exception which becomes a Failure, and return all other statuses unchanged. +- The status-to-typed-exception mapping step MUST treat only 400..599 as errors, mapping to the matching typed exception which becomes a Failure, and return all other statuses unchanged (RECOV-15). spec · `docs/product-spec/08-execution-pipelines.md:53` · high · sha:33e9443472ce -- Before mapping an error-status response, both initially and on a re-sent error response, the error body MUST be buffered into a bounded (1 MiB), replayable in-memory copy so the connection is released promptly and the body remains readable on the Failure, with the same bound shared across all buffering paths and the cap a hard truncation with no marker. +- Before mapping an error-status response, both initially and on a re-sent error response, the error body MUST be buffered into a bounded (1 MiB), replayable in-memory copy so the connection is released promptly and the body remains readable on the Failure, with the same bound shared across all buffering paths and the cap a hard truncation with no marker (RECOV-16). spec · `docs/product-spec/08-execution-pipelines.md:53` · high · sha:33e9443472ce - Pillar stages are validated at composition time to admit at most one step (PIPE-4/PIPE-5), distinguished by reference identity for idempotent re-installation (PIPE-6). design · `docs/sdk-design-nodejs/05-pipeline-architecture.md:20-21` · high · sha:16ad31311df7 @@ -119,11 +119,11 @@ design · `docs/sdk-design-nodejs/05-pipeline-architecture.md:50-53` · high · sha:16ad31311df7 ## Constraints -- A pillar stage MUST admit at most one step; the configurable pillars are REDIRECT, RETRY, AUTH, LOGGING, and SERDE. +- A pillar stage MUST admit at most one step; the configurable pillars are REDIRECT, RETRY, AUTH, LOGGING, and SERDE (PIPE-4). spec · `docs/product-spec/08-execution-pipelines.md:12` · high · sha:33e9443472ce -- The terminal SEND stage MUST be reserved for the transport hop, MUST NOT hold a user step, and flattening MUST skip it. +- The terminal SEND stage MUST be reserved for the transport hop, MUST NOT hold a user step, and flattening MUST skip it (PIPE-8). spec · `docs/product-spec/08-execution-pipelines.md:12` · high · sha:33e9443472ce -- The async standard pipeline MUST NOT follow HTTP redirects at the pipeline layer, since there is no async redirect pillar; a 3xx surfaces verbatim unless redirect following is enabled on the transport, and a port MUST document this asymmetry with the sync standard pipeline. +- The async standard pipeline MUST NOT follow HTTP redirects at the pipeline layer, since there is no async redirect pillar; a 3xx surfaces verbatim unless redirect following is enabled on the transport, and a port MUST document this asymmetry with the sync standard pipeline (PIPE-32). spec · `docs/product-spec/08-execution-pipelines.md:36` · high · sha:33e9443472ce - A port MUST NOT collapse the stage-based pipeline and recovery-chain primitives into one layer -- the stage pipeline owns ordering and re-drive-with-fork, while the recovery chain owns the sum-type fold and the uniform-failure guarantee. spec · `docs/product-spec/08-execution-pipelines.md:59` · high · sha:33e9443472ce @@ -133,13 +133,13 @@ ## Conclusions - The stage-based pipeline and the recovery-chain primitives share one backoff calculator and one pacing-header parser so their retry behavior cannot drift. spec · `docs/product-spec/08-execution-pipelines.md:3` · high · sha:33e9443472ce -- A closed two-variant outcome is what lets one code path handle a throwable and a response identically. +- A closed two-variant outcome is what lets one code path handle a throwable and a response identically (RECOV-1). spec · `docs/product-spec/08-execution-pipelines.md:45` · high · sha:33e9443472ce - The stage-based pipeline is used as the composition surface for assembling a client because it is where redirect, retry, auth, logging/instrumentation, and serialization concerns are ordered as pillar steps, where per-call cursors and forks drive re-attempts, and where a configured pipeline becomes a transport others can nest, and it has a real async mirror. spec · `docs/product-spec/08-execution-pipelines.md:57` · high · sha:33e9443472ce - The recovery-chain primitives are used as the resilience layer when a concern must observe every outcome uniformly, in particular error-mapping, retry, or rescue logic that must see a transport failure and a response failure through one code path and must never let a pre-transport throw bypass it; the recovery layer is synchronous, with its async equivalent expressed through the stage-based async pipeline. spec · `docs/product-spec/08-execution-pipelines.md:59` · high · sha:33e9443472ce -- The recovery-aware retry stack enforces a total-timeout budget that the stage-based retry step intentionally omits, and a port unifying retry entry points MUST make that budget explicitly opt-in. +- The recovery-aware retry stack enforces a total-timeout budget that the stage-based retry step intentionally omits, and a port unifying retry entry points MUST make that budget explicitly opt-in (RETRY-28). spec · `docs/product-spec/08-execution-pipelines.md:59` · high · sha:33e9443472ce - The stage-based pipeline (§8.1 of the spec) is structurally identical to the "onion" middleware composition pattern already implemented by every Koa-descended Node HTTP framework, including Koa itself, tRPC's middleware, and Apollo Server's plugin model. design · `docs/sdk-design-nodejs/05-pipeline-architecture.md:3-9` · high · sha:16ad31311df7 @@ -153,11 +153,11 @@ ## Reference - The SDK has two cooperating pipeline layers -- the stage-based pipeline, the user-facing dispatch runtime where cross-cutting concerns become discrete bidirectional steps on a fixed totally-ordered list of named stages, and the recovery-chain primitives, the resilience layer beneath resilience steps threading a closed two-variant outcome through a fold so every failure is observed uniformly. spec · `docs/product-spec/08-execution-pipelines.md:3` · high · sha:33e9443472ce -- The SERDE pillar is a reserved stage slot with no shipped behavior. +- The SERDE pillar is a reserved stage slot with no shipped behavior (PIPE-2). spec · `docs/product-spec/08-execution-pipelines.md:10` · high · sha:33e9443472ce -- Append-all MUST preserve the batch's iteration order within a stage, while prepend-all (each element prepended individually) results in the reversed batch order; a port MUST document this asymmetry. +- Append-all MUST preserve the batch's iteration order within a stage, while prepend-all (each element prepended individually) results in the reversed batch order; a port MUST document this asymmetry (PIPE-38). spec · `docs/product-spec/08-execution-pipelines.md:26` · high · sha:33e9443472ce -- In the reference implementation the request recovery chain does not defensively copy, retaining the caller's read-only list reference directly, an asymmetry a porter must not assume away; a port SHOULD copy there too. +- In the reference implementation the request recovery chain does not defensively copy, retaining the caller's read-only list reference directly, an asymmetry a porter must not assume away; a port SHOULD copy there too (RECOV-14). spec · `docs/product-spec/08-execution-pipelines.md:52` · high · sha:33e9443472ce - A pipeline step is a function of type `(request: Request, next: Next) => Promise`, where `Next = () => Promise`. design · `docs/sdk-design-nodejs/05-pipeline-architecture.md:12-16` · high · sha:16ad31311df7 diff --git a/docs/knowledge/redirect-handling.md b/docs/knowledge/redirect-handling.md index d8c1e90..a676317 100644 --- a/docs/knowledge/redirect-handling.md +++ b/docs/knowledge/redirect-handling.md @@ -1,59 +1,59 @@ # redirect-handling ## Rules -- A redirect is attempted only for status codes 301, 302, 303, 307, and 308; any other status, including 2xx, 4xx, 5xx, and non-redirect 3xx, is returned verbatim without consulting redirect logic. +- A redirect is attempted only for status codes 301, 302, 303, 307, and 308; any other status, including 2xx, 4xx, 5xx, and non-redirect 3xx, is returned verbatim without consulting redirect logic (REDIR-1). spec · `docs/product-spec/10-redirect-handling.md:7` · high · sha:f2a0d207be56 -- Status codes 300, 304, and 305 MUST NOT be auto-followed even with a Location header, and 305 in particular must never redirect to a server-chosen proxy. +- Status codes 300, 304, and 305 MUST NOT be auto-followed even with a Location header, and 305 in particular must never redirect to a server-chosen proxy (REDIR-2). spec · `docs/product-spec/10-redirect-handling.md:7` · high · sha:f2a0d207be56 -- For 301 and 302, a redirect is followed only if the original request method is in the configured allowed-method set (default {GET, HEAD}), and when followed, the original method and body are preserved, with deliberately no automatic POST-to-GET rewrite. +- For 301 and 302, a redirect is followed only if the original request method is in the configured allowed-method set (default {GET, HEAD}), and when followed, the original method and body are preserved, with deliberately no automatic POST-to-GET rewrite (REDIR-3). spec · `docs/product-spec/10-redirect-handling.md:8` · high · sha:f2a0d207be56 -- 307 and 308 redirects preserve method and body and are followed only if the method is in the allowed-method set. +- 307 and 308 redirects preserve method and body and are followed only if the method is in the allowed-method set (REDIR-4). spec · `docs/product-spec/10-redirect-handling.md:8` · high · sha:f2a0d207be56 -- 303 is not followed by default; when opted in it is re-issued as a GET with the body dropped and every Content-* request header, matched case-insensitively, removed, regardless of the original method. +- 303 is not followed by default; when opted in it is re-issued as a GET with the body dropped and every Content-* request header, matched case-insensitively, removed, regardless of the original method (REDIR-5). spec · `docs/product-spec/10-redirect-handling.md:8` · high · sha:f2a0d207be56 -- Any followed method-preserving redirect (301/302/307/308) re-sends the original body, so the body MUST be replayable; if present and not replayable, the operation MUST fail with a clear error naming replayability rather than corrupting or truncating the re-send, and the redirect is not attempted (303 is exempt because it drops the body). +- Any followed method-preserving redirect (301/302/307/308) re-sends the original body, so the body MUST be replayable; if present and not replayable, the operation MUST fail with a clear error naming replayability rather than corrupting or truncating the re-send, and the redirect is not attempted (303 is exempt because it drops the body) (REDIR-6). spec · `docs/product-spec/10-redirect-handling.md:9` · high · sha:f2a0d207be56 -- The Authorization header MUST be stripped before every redirect re-issue, including same-origin and the 303 GET rebuild, because re-attaching a credential for a known origin is the auth layer's job. +- The Authorization header MUST be stripped before every redirect re-issue, including same-origin and the 303 GET rebuild, because re-attaching a credential for a known origin is the auth layer's job (REDIR-7). spec · `docs/product-spec/10-redirect-handling.md:13` · high · sha:f2a0d207be56 -- A redirect is cross-origin if and only if the resolved target differs from the original (seed) request origin in scheme, host (case-insensitive), or effective port (scheme default when omitted); the comparison MUST be against the seed origin, not the previous hop, so a same-origin sub-redirect on a foreign host cannot re-expose the credential. +- A redirect is cross-origin if and only if the resolved target differs from the original (seed) request origin in scheme, host (case-insensitive), or effective port (scheme default when omitted); the comparison MUST be against the seed origin, not the previous hop, so a same-origin sub-redirect on a foreign host cannot re-expose the credential (REDIR-8). spec · `docs/product-spec/10-redirect-handling.md:13` · high · sha:f2a0d207be56 -- On a cross-origin redirect, whether method-preserving or a 303 GET rebuild, the origin-scoped Cookie and Proxy-Authorization headers MUST also be stripped. +- On a cross-origin redirect, whether method-preserving or a 303 GET rebuild, the origin-scoped Cookie and Proxy-Authorization headers MUST also be stripped (REDIR-9). spec · `docs/product-spec/10-redirect-handling.md:13` · high · sha:f2a0d207be56 -- On a same-origin redirect the Cookie header SHOULD be retained, with only Authorization stripped same-origin; a more conservative port MAY strip all cookies. +- On a same-origin redirect the Cookie header SHOULD be retained, with only Authorization stripped same-origin; a more conservative port MAY strip all cookies (REDIR-10). spec · `docs/product-spec/10-redirect-handling.md:13` · high · sha:f2a0d207be56 -- Because the auth layer runs inside the redirect loop, a cross-origin re-issue MUST carry an out-of-band signal instructing the auth layer to skip credential stamping; this signal MUST be impossible for a server-supplied Location to forge into a leak, MUST only suppress stamping and never cause a credential to be sent, and MUST be removed by the credential-attaching layer before dispatch; a same-origin re-issue is not signaled and is re-stamped normally. +- Because the auth layer runs inside the redirect loop, a cross-origin re-issue MUST carry an out-of-band signal instructing the auth layer to skip credential stamping; this signal MUST be impossible for a server-supplied Location to forge into a leak, MUST only suppress stamping and never cause a credential to be sent, and MUST be removed by the credential-attaching layer before dispatch; a same-origin re-issue is not signaled and is re-stamped normally (REDIR-11). spec · `docs/product-spec/10-redirect-handling.md:14` · high · sha:f2a0d207be56 -- The redirect layer clears any inbound copy of the cross-origin marker on every re-issue before conditionally setting its own on a cross-origin hop, making it impossible for a server-supplied Location to forge the marker. +- The redirect layer clears any inbound copy of the cross-origin marker on every re-issue before conditionally setting its own on a cross-origin hop, making it impossible for a server-supplied Location to forge the marker (REDIR-11). spec · `docs/product-spec/10-redirect-handling.md:14` · high · sha:f2a0d207be56 -- The redirect follower MUST wrap the auth layer, with redirect outer and auth inside per hop, which is what necessitates the Authorization-stripping and cross-origin-signal requirements. +- The redirect follower MUST wrap the auth layer, with redirect outer and auth inside per hop, which is what necessitates the Authorization-stripping and cross-origin-signal requirements (REDIR-24). spec · `docs/product-spec/10-redirect-handling.md:14` · high · sha:f2a0d207be56 -- Userinfo in the Location target (user:pass@) MUST be dropped before re-issue, and server-supplied embedded credentials MUST never be used. +- Userinfo in the Location target (user:pass@) MUST be dropped before re-issue, and server-supplied embedded credentials MUST never be used (REDIR-12). spec · `docs/product-spec/10-redirect-handling.md:15` · high · sha:f2a0d207be56 -- Stripping userinfo and resolving the Location generally MUST preserve the wire-exact, already-percent-encoded path, query, and fragment, and MUST preserve bracketed IPv6 literal hosts and explicit ports; re-encoding that would decode %2F to / or %26 to & is forbidden. +- Stripping userinfo and resolving the Location generally MUST preserve the wire-exact, already-percent-encoded path, query, and fragment, and MUST preserve bracketed IPv6 literal hosts and explicit ports; re-encoding that would decode %2F to / or %26 to & is forbidden (REDIR-13). spec · `docs/product-spec/10-redirect-handling.md:15` · high · sha:f2a0d207be56 -- A relative Location MUST be resolved against the current hop's request URL per RFC 3986; absolute values are used as-is after userinfo stripping. +- A relative Location MUST be resolved against the current hop's request URL per RFC 3986; absolute values are used as-is after userinfo stripping (REDIR-14). spec · `docs/product-spec/10-redirect-handling.md:19` · high · sha:f2a0d207be56 -- An HTTPS-to-HTTP scheme downgrade across a single hop MUST be rejected by default, failing with a clear error, and permitted only via an opt-in that surfaces the downgrade observably; credential stripping applies regardless, and the check is evaluated per hop. +- An HTTPS-to-HTTP scheme downgrade across a single hop MUST be rejected by default, failing with a clear error, and permitted only via an opt-in that surfaces the downgrade observably; credential stripping applies regardless, and the check is evaluated per hop (REDIR-15). spec · `docs/product-spec/10-redirect-handling.md:19` · high · sha:f2a0d207be56 -- The redirect step MUST detect redirect loops by recording every visited absolute URI, seeded with the original request URI, and when a redirect would revisit a seen URI, MUST stop and return the current redirect response without throwing, leaving its body open for the caller. +- The redirect step MUST detect redirect loops by recording every visited absolute URI, seeded with the original request URI, and when a redirect would revisit a seen URI, MUST stop and return the current redirect response without throwing, leaving its body open for the caller (REDIR-16). spec · `docs/product-spec/10-redirect-handling.md:20` · high · sha:f2a0d207be56 -- The number of followed redirects MUST be capped by max-hops (default 3); on reaching the cap the last response is returned as-is even if itself a 3xx, without throwing, and max-hops 0 MUST disable redirect following entirely. +- The number of followed redirects MUST be capped by max-hops (default 3); on reaching the cap the last response is returned as-is even if itself a 3xx, without throwing, and max-hops 0 MUST disable redirect following entirely (REDIR-17). spec · `docs/product-spec/10-redirect-handling.md:20` · high · sha:f2a0d207be56 -- A malformed or unresolvable Location, such as an invalid URI, illegal characters, or an unsupported scheme, MUST NOT throw; the step logs it and returns the current redirect response unfollowed. +- A malformed or unresolvable Location, such as an invalid URI, illegal characters, or an unsupported scheme, MUST NOT throw; the step logs it and returns the current redirect response unfollowed (REDIR-18). spec · `docs/product-spec/10-redirect-handling.md:21` · high · sha:f2a0d207be56 -- A redirect response with a missing or empty Location MUST be returned unfollowed. +- A redirect response with a missing or empty Location MUST be returned unfollowed (REDIR-19). spec · `docs/product-spec/10-redirect-handling.md:21` · high · sha:f2a0d207be56 -- The redirect step MUST manage response-body lifecycle deterministically -- before issuing a follow-up the prior redirect response's body MUST be closed; if building the follow-up throws (non-replayable body, downgrade rejection) the current response MUST be closed before the error propagates; on any 'return current' outcome the returned response is left open for the caller. +- The redirect step MUST manage response-body lifecycle deterministically -- before issuing a follow-up the prior redirect response's body MUST be closed; if building the follow-up throws (non-replayable body, downgrade rejection) the current response MUST be closed before the error propagates; on any 'return current' outcome the returned response is left open for the caller (REDIR-22). spec · `docs/product-spec/10-redirect-handling.md:22` · high · sha:f2a0d207be56 -- Redirect following SHOULD be an iterative loop, not unbounded recursion, so it is stack-safe. +- Redirect following SHOULD be an iterative loop, not unbounded recursion, so it is stack-safe (REDIR-23). spec · `docs/product-spec/10-redirect-handling.md:22` · high · sha:f2a0d207be56 -- A configured redirect predicate MUST fully override the built-in decision and receive a read-only, defensively-copied condition snapshot containing the current response, the count of redirects already followed, and an insertion-ordered set of visited URIs including the current request's, so it cannot mutate the live cycle-detection state. +- A configured redirect predicate MUST fully override the built-in decision and receive a read-only, defensively-copied condition snapshot containing the current response, the count of redirects already followed, and an insertion-ordered set of visited URIs including the current request's, so it cannot mutate the live cycle-detection state (REDIR-20). spec · `docs/product-spec/10-redirect-handling.md:26` · high · sha:f2a0d207be56 -- On the non-redirect fast path, a status that is not a recognized redirect code, the implementation SHOULD short-circuit before allocating a condition snapshot and MUST NOT consult the predicate; but a recognized 3xx always allocates the snapshot and consults the predicate, even with no usable Location. +- On the non-redirect fast path, a status that is not a recognized redirect code, the implementation SHOULD short-circuit before allocating a condition snapshot and MUST NOT consult the predicate; but a recognized 3xx always allocates the snapshot and consults the predicate, even with no usable Location (REDIR-21). spec · `docs/product-spec/10-redirect-handling.md:26` · high · sha:f2a0d207be56 -- The configured allowed-method set MUST be stored as an immutable defensive copy so post-construction mutation of the caller's collection cannot change policy. +- The configured allowed-method set MUST be stored as an immutable defensive copy so post-construction mutation of the caller's collection cannot change policy (REDIR-26). spec · `docs/product-spec/10-redirect-handling.md:27` · high · sha:f2a0d207be56 -- Each followed hop, loop detection, and scheme-downgrade event SHOULD be emitted as structured records with URLs passed through a redactor, redaction failures degrading to a placeholder rather than crashing logging; the malformed-Location event is the exception, logging the raw Location string as received since it failed to parse and cannot be redacted. +- Each followed hop, loop detection, and scheme-downgrade event SHOULD be emitted as structured records with URLs passed through a redactor, redaction failures degrading to a placeholder rather than crashing logging; the malformed-Location event is the exception, logging the raw Location string as received since it failed to parse and cannot be redacted (REDIR-28). spec · `docs/product-spec/10-redirect-handling.md:27` · high · sha:f2a0d207be56 ## Constraints @@ -63,11 +63,11 @@ design · `docs/sdk-design-nodejs/06-retry-redirect-and-authentication.md:49-53` · high · sha:b0e2bb42d809 ## Reference -- Redirect following is a synchronous pillar step coordinating with the auth pillar via an internal cross-origin marker; the async pipeline follows no redirects. +- Redirect following is a synchronous pillar step coordinating with the auth pillar via an internal cross-origin marker; the async pipeline follows no redirects (REDIR-25). spec · `docs/product-spec/10-redirect-handling.md:3` · high · sha:f2a0d207be56 -- Only the auth step strips the internal cross-origin marker in the reference implementation, so a pipeline with no auth step, including the sync standard-resilience preset, forwards the internal marker to the transport; a robust port should strip the signal independently of whether a credential layer runs. +- Only the auth step strips the internal cross-origin marker in the reference implementation, so a pipeline with no auth step, including the sync standard-resilience preset, forwards the internal marker to the transport; a robust port should strip the signal independently of whether a credential layer runs (REDIR-11). spec · `docs/product-spec/10-redirect-handling.md:14` · high · sha:f2a0d207be56 -- The header the redirect target is read from MAY be configurable, defaulting to Location. +- The header the redirect target is read from MAY be configurable, defaulting to Location (REDIR-27). spec · `docs/product-spec/10-redirect-handling.md:27` · high · sha:f2a0d207be56 - Cross-origin detection in the port compares `new URL(target).origin` against the seed origin after normalizing default ports. design · `docs/sdk-design-nodejs/06-retry-redirect-and-authentication.md:53-55` · high · sha:b0e2bb42d809 diff --git a/docs/knowledge/retry-and-resilience.md b/docs/knowledge/retry-and-resilience.md index ae88bc0..cb8dd75 100644 --- a/docs/knowledge/retry-and-resilience.md +++ b/docs/knowledge/retry-and-resilience.md @@ -5,83 +5,83 @@ spec · `docs/product-spec/06-request-and-response-body-lifecycle.md:18-18` · high · sha:c2bf15dc8a06 - On the retry path specifically, a body-less request's re-send eligibility MUST gate on method idempotency rather than replayability, so only idempotent methods are retried when there is no body, meaning a body-less non-idempotent POST is not retried (BODY-5). spec · `docs/product-spec/06-request-and-response-body-lifecycle.md:19-19` · high · sha:c2bf15dc8a06 -- The retryable-status classifier MUST be single-sourced and treat exactly 408, 429, and all of 500-599 except 501 and 505 as retryable; this is the single definition the response-carrying exception flag and the stage stack's default predicate derive from, while the recovery stack layers its own configurable status allow-list on top. +- The retryable-status classifier MUST be single-sourced and treat exactly 408, 429, and all of 500-599 except 501 and 505 as retryable; this is the single definition the response-carrying exception flag and the stage stack's default predicate derive from, while the recovery stack layers its own configurable status allow-list on top (RETRY-1). spec · `docs/product-spec/09-retry-and-resilience.md:9` · high · sha:9efbe276001e -- The retryable-throwable set MUST be defined in exactly one place -- any throwable that is, or has anywhere in its cause chain, an I/O error or a timeout error, found via an iterative, identity-tracking cause-chain walk that terminates on a cyclic chain. +- The retryable-throwable set MUST be defined in exactly one place -- any throwable that is, or has anywhere in its cause chain, an I/O error or a timeout error, found via an iterative, identity-tracking cause-chain walk that terminates on a cyclic chain (RETRY-2). spec · `docs/product-spec/09-retry-and-resilience.md:10` · high · sha:9efbe276001e -- A response-carrying exception MUST derive its own retryable flag from the single status classifier at construction, not a hardcoded per-subclass constant. +- A response-carrying exception MUST derive its own retryable flag from the single status classifier at construction, not a hardcoded per-subclass constant (RETRY-3). spec · `docs/product-spec/09-retry-and-resilience.md:10` · high · sha:9efbe276001e -- A transport-level failure that produced no complete response, such as connection refused, TLS/DNS failure, socket read timeout, or peer reset, MUST be classified retryable unconditionally at the condition level, with safety gated separately. +- A transport-level failure that produced no complete response, such as connection refused, TLS/DNS failure, socket read timeout, or peer reset, MUST be classified retryable unconditionally at the condition level, with safety gated separately (RETRY-4). spec · `docs/product-spec/09-retry-and-resilience.md:10` · high · sha:9efbe276001e -- A request is re-sendable if and only if it has no body and its method is idempotent, or it has a body and that body is replayable; both retry stacks MUST apply this identical rule. +- A request is re-sendable if and only if it has no body and its method is idempotent, or it has a body and that body is replayable; both retry stacks MUST apply this identical rule (RETRY-5 / RECOV-18). spec · `docs/product-spec/09-retry-and-resilience.md:11` · high · sha:9efbe276001e -- When a request is not re-sendable, the retry logic MUST perform exactly one attempt and MUST NOT retry, even when the condition is retryable and even when there is no body to physically re-send (a bare non-idempotent POST). +- When a request is not re-sendable, the retry logic MUST perform exactly one attempt and MUST NOT retry, even when the condition is retryable and even when there is no body to physically re-send (a bare non-idempotent POST) (RETRY-7). spec · `docs/product-spec/09-retry-and-resilience.md:12` · high · sha:9efbe276001e -- Retry eligibility MUST require both a retryable condition and a re-sendable request; neither implies the other. +- Retry eligibility MUST require both a retryable condition and a re-sendable request; neither implies the other (RETRY-8). spec · `docs/product-spec/09-retry-and-resilience.md:12` · high · sha:9efbe276001e -- The unjittered exponential delay MUST be initialDelay times multiplier raised to (attempt minus 1), with attempt 1-indexed such that attempt 1 is the wait before the first retry, clamped to a maximum delay cap. +- The unjittered exponential delay MUST be initialDelay times multiplier raised to (attempt minus 1), with attempt 1-indexed such that attempt 1 is the wait before the first retry, clamped to a maximum delay cap (RETRY-9 / RECOV-21). spec · `docs/product-spec/09-retry-and-resilience.md:16` · high · sha:9efbe276001e -- Symmetric jitter MUST draw the effective delay uniformly from [d*(1-j/2), d*(1+j/2)] with midpoint d, j=0 returning d, j constrained to [0,1], a degenerate sub-nanosecond range returning the base delay, and a negative sample floored to zero. +- Symmetric jitter MUST draw the effective delay uniformly from [d*(1-j/2), d*(1+j/2)] with midpoint d, j=0 returning d, j constrained to [0,1], a degenerate sub-nanosecond range returning the base delay, and a negative sample floored to zero (RETRY-10). spec · `docs/product-spec/09-retry-and-resilience.md:16` · high · sha:9efbe276001e -- Delay computation MUST be overflow-safe, saturating to the cap rather than throwing, and MUST reject an attempt value less than 1. +- Delay computation MUST be overflow-safe, saturating to the cap rather than throwing, and MUST reject an attempt value less than 1 (RETRY-11). spec · `docs/product-spec/09-retry-and-resilience.md:16` · high · sha:9efbe276001e -- The pacing-header parser MUST recognize Retry-After as delta-seconds (integer and fractional), Retry-After as an RFC 1123 HTTP-date tolerant of an informational weekday and single-digit day, retry-after-ms and x-ms-retry-after-ms as integer milliseconds, and X-RateLimit-Reset as Unix epoch seconds whose delta is positively jittered to [100%,120%]. +- The pacing-header parser MUST recognize Retry-After as delta-seconds (integer and fractional), Retry-After as an RFC 1123 HTTP-date tolerant of an informational weekday and single-digit day, retry-after-ms and x-ms-retry-after-ms as integer milliseconds, and X-RateLimit-Reset as Unix epoch seconds whose delta is positively jittered to [100%,120%] (RETRY-15 / RECOV-24 / RECOV-25). spec · `docs/product-spec/09-retry-and-resilience.md:17` · high · sha:9efbe276001e -- The pacing-header parser MUST be total and never throw; malformed, negative, or out-of-range values MUST map to no hint (null), not a zero delay, so the caller falls back to backoff rather than hammering the server. +- The pacing-header parser MUST be total and never throw; malformed, negative, or out-of-range values MUST map to no hint (null), not a zero delay, so the caller falls back to backoff rather than hammering the server (RETRY-16 / RECOV-23). spec · `docs/product-spec/09-retry-and-resilience.md:17` · high · sha:9efbe276001e -- A valid HTTP-date or epoch value already in the past MUST yield a zero delay (retry immediately), distinct from an unparseable value which yields no hint. +- A valid HTTP-date or epoch value already in the past MUST yield a zero delay (retry immediately), distinct from an unparseable value which yields no hint (RETRY-17). spec · `docs/product-spec/09-retry-and-resilience.md:17` · high · sha:9efbe276001e -- Any computed pacing delta MUST be clamped to a finite ceiling of 365 days before nanosecond conversion. +- Any computed pacing delta MUST be clamped to a finite ceiling of 365 days before nanosecond conversion (RETRY-18 / RECOV-26). spec · `docs/product-spec/09-retry-and-resilience.md:17` · high · sha:9efbe276001e -- Numeric Retry-After parsing MUST be screened by a strict decimal grammar before any float parse, rejecting type-suffixed, hex-float, NaN, and Infinity forms. +- Numeric Retry-After parsing MUST be screened by a strict decimal grammar before any float parse, rejecting type-suffixed, hex-float, NaN, and Infinity forms (RETRY-19). spec · `docs/product-spec/09-retry-and-resilience.md:17` · high · sha:9efbe276001e -- A present pacing hint MUST override (replace, not augment) the exponential schedule for that single decision; a literal Retry-After hint MUST NOT receive additional symmetric jitter, and where a total-timeout deadline applies the hint MUST still be clamped against it. +- A present pacing hint MUST override (replace, not augment) the exponential schedule for that single decision; a literal Retry-After hint MUST NOT receive additional symmetric jitter, and where a total-timeout deadline applies the hint MUST still be clamped against it (RETRY-20 / RECOV-22). spec · `docs/product-spec/09-retry-and-resilience.md:18` · high · sha:9efbe276001e -- Pacing resolution MUST honor a defined precedence and return the first parseable value -- the recovery stack scans the whole header map with fixed precedence Retry-After numeric then date, then retry-after-ms, then x-ms-retry-after-ms, then X-RateLimit-Reset, while the stage stack walks a caller-configurable ordered header list. +- Pacing resolution MUST honor a defined precedence and return the first parseable value -- the recovery stack scans the whole header map with fixed precedence Retry-After numeric then date, then retry-after-ms, then x-ms-retry-after-ms, then X-RateLimit-Reset, while the stage stack walks a caller-configurable ordered header list (RETRY-21). spec · `docs/product-spec/09-retry-and-resilience.md:18` · high · sha:9efbe276001e -- A failure while parsing a pacing header MUST NOT mask the real upstream failure; the loop falls back to exponential backoff and the original throwable remains the surfaced error. +- A failure while parsing a pacing header MUST NOT mask the real upstream failure; the loop falls back to exponential backoff and the original throwable remains the surfaced error (RETRY-22 / RECOV-29). spec · `docs/product-spec/09-retry-and-resilience.md:18` · high · sha:9efbe276001e -- Thread interruption/cancellation MUST never be treated as a retryable failure; on interrupt during a blocking backoff wait, the implementation MUST restore the cancellation flag, cancel any externally-scheduled wake, abort the retry loop, and surface an interrupted-I/O error, and a downstream interrupt surfaced as an interrupted-I/O error is treated as terminal cancellation, not retried. +- Thread interruption/cancellation MUST never be treated as a retryable failure; on interrupt during a blocking backoff wait, the implementation MUST restore the cancellation flag, cancel any externally-scheduled wake, abort the retry loop, and surface an interrupted-I/O error, and a downstream interrupt surfaced as an interrupted-I/O error is treated as terminal cancellation, not retried (RETRY-23). spec · `docs/product-spec/09-retry-and-resilience.md:22` · high · sha:9efbe276001e -- A read-timeout represented as a subtype of the interrupted-I/O error MUST NOT be mistaken for cancellation; it remains a retryable condition. +- A read-timeout represented as a subtype of the interrupted-I/O error MUST NOT be mistaken for cancellation; it remains a retryable condition (RETRY-24). spec · `docs/product-spec/09-retry-and-resilience.md:22` · high · sha:9efbe276001e -- Non-recoverable runtime errors such as out-of-memory and stack overflow MUST NOT be retried, classified retryable, or logged; they MUST be surfaced unchanged with no suppressed-trail attachment. +- Non-recoverable runtime errors such as out-of-memory and stack overflow MUST NOT be retried, classified retryable, or logged; they MUST be surfaced unchanged with no suppressed-trail attachment (RETRY-25). spec · `docs/product-spec/09-retry-and-resilience.md:22` · high · sha:9efbe276001e -- The inter-attempt wait MUST be cancellable/interruptible and MUST NOT pin an execution carrier for its duration; a naive uninterruptible sleep that cannot be cancelled is non-conforming. +- The inter-attempt wait MUST be cancellable/interruptible and MUST NOT pin an execution carrier for its duration; a naive uninterruptible sleep that cannot be cancelled is non-conforming (RETRY-26 / RECOV-27). spec · `docs/product-spec/09-retry-and-resilience.md:23` · high · sha:9efbe276001e -- The recovery stack MUST enforce an optional total-timeout budget with per-attempt deadline shrinking, aborting before each attempt if the attempt cap is reached, if elapsed time is at or beyond the budget, or if elapsed plus the next delay would exceed the budget, clamping the delay so it cannot overshoot, with a zero budget disabling the deadline. +- The recovery stack MUST enforce an optional total-timeout budget with per-attempt deadline shrinking, aborting before each attempt if the attempt cap is reached, if elapsed time is at or beyond the budget, or if elapsed plus the next delay would exceed the budget, clamping the delay so it cannot overshoot, with a zero budget disabling the deadline (RETRY-27 / RECOV-20). spec · `docs/product-spec/09-retry-and-resilience.md:27` · high · sha:9efbe276001e -- Both retry stacks MUST compute backoff via one shared calculator and shared constants, and their attempt budgets MUST denote the same number of total wire sends under equivalent defaults. +- Both retry stacks MUST compute backoff via one shared calculator and shared constants, and their attempt budgets MUST denote the same number of total wire sends under equivalent defaults (RETRY-13 / RETRY-14 / RECOV-30). spec · `docs/product-spec/09-retry-and-resilience.md:28` · high · sha:9efbe276001e -- In the recovery stack, for a failure carrying a received response, the configured retryable-status set MUST be authoritative, able to both widen and narrow relative to the built-in classifier, while a no-response transport failure falls back to its always-retryable flag. +- In the recovery stack, for a failure carrying a received response, the configured retryable-status set MUST be authoritative, able to both widen and narrow relative to the built-in classifier, while a no-response transport failure falls back to its always-retryable flag (RETRY-37 / RECOV-17). spec · `docs/product-spec/09-retry-and-resilience.md:29` · high · sha:9efbe276001e -- A re-sent response whose error status is in the configured retryable-status set MUST be re-mapped into a typed failure so the loop keeps evaluating the budget, e.g. a 503,503,200 sequence reaches the 200; all other re-sent responses pass through as Success. +- A re-sent response whose error status is in the configured retryable-status set MUST be re-mapped into a typed failure so the loop keeps evaluating the budget, e.g. a 503,503,200 sequence reaches the 200; all other re-sent responses pass through as Success (RETRY-36 / RECOV-19). spec · `docs/product-spec/09-retry-and-resilience.md:29` · high · sha:9efbe276001e -- A retryable response's body/connection MUST be released before the backoff wait so a socket is not pinned across the delay; the pacing delay is computed from the still-open response first, and if the retry decision or delay computation throws, the response MUST still be closed before propagating. +- A retryable response's body/connection MUST be released before the backoff wait so a socket is not pinned across the delay; the pacing delay is computed from the still-open response first, and if the retry decision or delay computation throws, the response MUST still be closed before propagating (RETRY-35). spec · `docs/product-spec/09-retry-and-resilience.md:30` · high · sha:9efbe276001e -- On terminal failure, every prior failed attempt's exception MUST be attached to the surfaced exception as suppressed, skipping the surfaced instance itself so a reused exception instance cannot trip a self-suppression error, and on eventual success the prior trail MUST be discarded. +- On terminal failure, every prior failed attempt's exception MUST be attached to the surfaced exception as suppressed, skipping the surfaced instance itself so a reused exception instance cannot trip a self-suppression error, and on eventual success the prior trail MUST be discarded (RETRY-34). spec · `docs/product-spec/09-retry-and-resilience.md:30` · high · sha:9efbe276001e -- The asynchronous retry loop MUST be driven by an iterative trampoline; N retries MUST NOT build an N-deep chain of future continuations or stack frames, and a completion warranting another attempt hands control to a single active pump via a re-arm flag rather than recursing. +- The asynchronous retry loop MUST be driven by an iterative trampoline; N retries MUST NOT build an N-deep chain of future continuations or stack frames, and a completion warranting another attempt hands control to a single active pump via a re-arm flag rather than recursing (RETRY-30). spec · `docs/product-spec/09-retry-and-resilience.md:34` · high · sha:9efbe276001e -- Async backoff delays MUST be scheduled non-blockingly, with a zero-length delay completing inline and re-arming the active pump. +- Async backoff delays MUST be scheduled non-blockingly, with a zero-length delay completing inline and re-arming the active pump (RETRY-31). spec · `docs/product-spec/09-retry-and-resilience.md:34` · high · sha:9efbe276001e -- If the caller has already completed or cancelled the returned async result, the driver MUST launch no further attempts, and any response arriving from an in-flight attempt MUST be closed rather than leaked. +- If the caller has already completed or cancelled the returned async result, the driver MUST launch no further attempts, and any response arriving from an in-flight attempt MUST be closed rather than leaked (RETRY-32). spec · `docs/product-spec/09-retry-and-resilience.md:34` · high · sha:9efbe276001e -- Every terminal path of the async retry loop MUST complete the returned future, with a throwing predicate, delay computation, log call, or synchronous scheduler rejection each completing it exceptionally, closing any open retryable response first. +- Every terminal path of the async retry loop MUST complete the returned future, with a throwing predicate, delay computation, log call, or synchronous scheduler rejection each completing it exceptionally, closing any open retryable response first (RETRY-33). spec · `docs/product-spec/09-retry-and-resilience.md:34` · high · sha:9efbe276001e -- The stage stack's delay resolution MUST follow the precedence caller delay-override, then server pacing headers (response path only), then fixed delay, then exponential backoff, with the exception path skipping the header step. +- The stage stack's delay resolution MUST follow the precedence caller delay-override, then server pacing headers (response path only), then fixed delay, then exponential backoff, with the exception path skipping the header step (RETRY-39). spec · `docs/product-spec/09-retry-and-resilience.md:35` · high · sha:9efbe276001e -- A throwing user delay-override SHOULD be non-fatal, logging and falling back, while a throwing should-retry predicate SHOULD abort the call as a well-typed error, with fatal errors rethrown unchanged in both cases. +- A throwing user delay-override SHOULD be non-fatal, logging and falling back, while a throwing should-retry predicate SHOULD abort the call as a well-typed error, with fatal errors rethrown unchanged in both cases (RETRY-40). spec · `docs/product-spec/09-retry-and-resilience.md:35` · high · sha:9efbe276001e -- The stage stack MUST resolve the effective retry count as present-override-wins (validated non-negative), else the configured value, with a negative configured value clamped to the default and zero meaning no retries. +- The stage stack MUST resolve the effective retry count as present-override-wins (validated non-negative), else the configured value, with a negative configured value clamped to the default and zero meaning no retries (RETRY-41). spec · `docs/product-spec/09-retry-and-resilience.md:35` · high · sha:9efbe276001e -- All retry policy components MUST be immutable and stateless after construction and safe for concurrent invocation, with every piece of per-call state on the per-call stack/driver, never the shared instance. +- All retry policy components MUST be immutable and stateless after construction and safe for concurrent invocation, with every piece of per-call state on the per-call stack/driver, never the shared instance (RETRY-42 / RECOV-28). spec · `docs/product-spec/09-retry-and-resilience.md:35` · high · sha:9efbe276001e -- Each retry attempt MUST re-execute the downstream chain with fresh per-attempt continuation state rather than reusing the prior attempt's in-flight chain, and upstream steps MUST NOT mutate the shared in-flight request between attempts. +- Each retry attempt MUST re-execute the downstream chain with fresh per-attempt continuation state rather than reusing the prior attempt's in-flight chain, and upstream steps MUST NOT mutate the shared in-flight request between attempts (RETRY-44). spec · `docs/product-spec/09-retry-and-resilience.md:35` · high · sha:9efbe276001e -- The retry engine MUST NOT shut down or close a caller-supplied scheduler, and a process-wide default scheduler, when used, is likewise never shut down by the SDK. +- The retry engine MUST NOT shut down or close a caller-supplied scheduler, and a process-wide default scheduler, when used, is likewise never shut down by the SDK (RETRY-45). spec · `docs/product-spec/09-retry-and-resilience.md:35` · high · sha:9efbe276001e -- Retry-safety must be decided at the retry step independently of retryability and applied uniformly to protocol and transport failures, so a body-less request is retry-safe only if its method is idempotent (a bare POST is never retried even on a transport error) and a body-bearing request is retry-safe only if its body is replayable (a single-use/streaming body is never re-sent). +- Retry-safety must be decided at the retry step independently of retryability and applied uniformly to protocol and transport failures, so a body-less request is retry-safe only if its method is idempotent (a bare POST is never retried even on a transport error) and a body-bearing request is retry-safe only if its body is replayable (a single-use/streaming body is never re-sent) (XCUT-10). spec · `docs/product-spec/19-cross-cutting-invariants-and-policies.md:24` · high · sha:d6123be82c9e - The backoff calculator must apply overflow-safe saturation to the delay cap rather than throwing. design · `docs/sdk-design-nodejs/06-retry-redirect-and-authentication.md:13-14` · high · sha:b0e2bb42d809 @@ -89,7 +89,7 @@ design · `docs/sdk-design-nodejs/06-retry-redirect-and-authentication.md:33-36` · high · sha:b0e2bb42d809 ## Constraints -- The stage-based retry stack MUST NOT impose a total-timeout budget; a port that unifies the stacks MUST make the total-timeout an explicitly opt-in feature rather than always-on. +- The stage-based retry stack MUST NOT impose a total-timeout budget; a port that unifies the stacks MUST make the total-timeout an explicitly opt-in feature rather than always-on (RETRY-28). spec · `docs/product-spec/09-retry-and-resilience.md:27` · high · sha:9efbe276001e ## Conclusions @@ -97,7 +97,7 @@ spec · `docs/product-spec/06-request-and-response-body-lifecycle.md:18-18` · high · sha:c2bf15dc8a06 - The body-less idempotency gate is retry-specific; the redirect path re-sends body-less requests per redirect semantics and does not consult idempotency (BODY-5). spec · `docs/product-spec/06-request-and-response-body-lifecycle.md:19-19` · high · sha:c2bf15dc8a06 -- 501 and 505 are excluded from the retryable status set because they mean the server cannot fulfill the request regardless of retry. +- 501 and 505 are excluded from the retryable status set because they mean the server cannot fulfill the request regardless of retry (RETRY-1). spec · `docs/product-spec/09-retry-and-resilience.md:9` · high · sha:9efbe276001e - The port single-sources the idempotent-method set, retryable-status set, and shared backoff calculator in one ES module because ES modules are singletons by default, unlike JVM classloaders which can each load their own copy of a class. design · `docs/sdk-design-nodejs/06-retry-redirect-and-authentication.md:1-8` · high · sha:b0e2bb42d809 @@ -113,25 +113,25 @@ spec · `docs/product-spec/09-retry-and-resilience.md:3` · high · sha:9efbe276001e - Retry happens only when both a retryable condition and a re-sendable request hold; the two axes are orthogonal. spec · `docs/product-spec/09-retry-and-resilience.md:7` · high · sha:9efbe276001e -- The idempotent-method set MUST be single-sourced and equal to {GET, HEAD, OPTIONS, PUT, DELETE}; POST and PATCH are re-sendable only via the replayable-body path. +- The idempotent-method set MUST be single-sourced and equal to {GET, HEAD, OPTIONS, PUT, DELETE}; POST and PATCH are re-sendable only via the replayable-body path (RETRY-6). spec · `docs/product-spec/09-retry-and-resilience.md:11` · high · sha:9efbe276001e -- Default retry tuning SHOULD be an initial delay of 200 ms, a multiplier of 2.0, a max delay of 8 s, a jitter of 0.2, and a budget of 3 sends. +- Default retry tuning SHOULD be an initial delay of 200 ms, a multiplier of 2.0, a max delay of 8 s, a jitter of 0.2, and a budget of 3 sends (RETRY-12). spec · `docs/product-spec/09-retry-and-resilience.md:16` · high · sha:9efbe276001e -- The recovery stack schedules the wake on a shared scheduler and blocks on the resulting future; the stage-sync stack performs an interruptible sleep that unmounts a virtual-thread carrier; async implementations schedule the delay without blocking a thread. +- The recovery stack schedules the wake on a shared scheduler and blocks on the resulting future; the stage-sync stack performs an interruptible sleep that unmounts a virtual-thread carrier; async implementations schedule the delay without blocking a thread (RETRY-26). spec · `docs/product-spec/09-retry-and-resilience.md:23` · high · sha:9efbe276001e -- The recovery stack's max-attempts default of 3 equals the stage stack's default max-retries of 2 plus one initial send. +- The recovery stack's max-attempts default of 3 equals the stage stack's default max-retries of 2 plus one initial send (RETRY-14). spec · `docs/product-spec/09-retry-and-resilience.md:28` · high · sha:9efbe276001e -- The recovery stack's configured retryable-status set is authoritative-contains, not an intersection with the baked-in classifier flag; a port should follow that. +- The recovery stack's configured retryable-status set is authoritative-contains, not an intersection with the baked-in classifier flag; a port should follow that (RETRY-37). spec · `docs/product-spec/09-retry-and-resilience.md:29` · high · sha:9efbe276001e -- Only the async retry stack currently implements the skip-self suppression guard in the reference implementation; a port MUST apply it to both stacks. +- Only the async retry stack currently implements the skip-self suppression guard in the reference implementation; a port MUST apply it to both stacks (RETRY-34). spec · `docs/product-spec/09-retry-and-resilience.md:30` · high · sha:9efbe276001e -- A fixed-delay configuration MAY force a flat delay disabling backoff and jitter, making the backoff path unreachable. +- A fixed-delay configuration MAY force a flat delay disabling backoff and jitter, making the backoff path unreachable (RETRY-43). spec · `docs/product-spec/09-retry-and-resilience.md:35` · high · sha:9efbe276001e -- An opt-in server-driven override MAY let a response header force or suppress the retry classification, flipping only classification and remaining subject to the attempt cap and the re-send-safety gate. +- An opt-in server-driven override MAY let a response header force or suppress the retry classification, flipping only classification and remaining subject to the attempt cap and the re-send-safety gate (RETRY-29). spec · `docs/product-spec/09-retry-and-resilience.md:35` · high · sha:9efbe276001e -- An optional per-attempt request header MAY stamp the 1-based attempt ordinal on a fresh per-attempt copy, never mutating the captured template and preserving any idempotency key, allocating nothing when disabled. +- An optional per-attempt request header MAY stamp the 1-based attempt ordinal on a fresh per-attempt copy, never mutating the captured template and preserving any idempotency key, allocating nothing when disabled (RETRY-38 / RECOV-31). spec · `docs/product-spec/09-retry-and-resilience.md:35` · high · sha:9efbe276001e -- A shared retryability classifier should treat HTTP status 408, 429, and all 5xx except 501/505 as retryable, and should treat a throwable as retryable iff it or any cause in its chain is an IO/timeout error (cause-chain traversal cycle-safe); where implemented, this exact status set is a hard contract. +- A shared retryability classifier should treat HTTP status 408, 429, and all 5xx except 501/505 as retryable, and should treat a throwable as retryable iff it or any cause in its chain is an IO/timeout error (cause-chain traversal cycle-safe); where implemented, this exact status set is a hard contract (CFG-35). spec · `docs/product-spec/16-configuration.md:58-58` · high · sha:367e27ec6481 - An idempotent method is an HTTP method whose repetition has the same effect as a single invocation; the SDK's idempotent set is `{GET, HEAD, OPTIONS, PUT, DELETE}`, used as the retry-safety gate for body-less requests. spec · `docs/product-spec/appendix-a-glossary.md:31` · high · sha:f0b3d2058626 diff --git a/docs/knowledge/seams-and-extensibility.md b/docs/knowledge/seams-and-extensibility.md index 1f54a67..83f39d7 100644 --- a/docs/knowledge/seams-and-extensibility.md +++ b/docs/knowledge/seams-and-extensibility.md @@ -5,7 +5,7 @@ spec · `docs/product-spec/01-product-overview.md:9-9` · high · sha:4f786c44354d - The byte-stream provider MUST expose factory operations to create a new empty in-memory buffer, a buffered reader over a raw input stream, a buffered reader over a byte array, a buffered writer over a raw output stream, and wrappers that add the buffered surface to a primitive source/sink (SEAM-3). spec · `docs/product-spec/03-pluggable-seams-and-extension-model.md:7-7` · high · sha:0adae2d6a47f -- A reader/writer created over a caller's raw stream takes ownership of that stream, so closing the reader/writer closes the underlying stream (SEAM-3). +- A reader/writer created over a caller's raw stream takes ownership of that stream, so closing the reader/writer closes the underlying stream (SEAM-3 / IO-6). spec · `docs/product-spec/03-pluggable-seams-and-extension-model.md:7-7` · high · sha:0adae2d6a47f - The synchronous transport MUST be a single-operation contract — given one request, produce one response — and MUST NOT pre-buffer the response body, leaving the caller to own reading and closing it (SEAM-11). spec · `docs/product-spec/03-pluggable-seams-and-extension-model.md:12-12` · high · sha:0adae2d6a47f @@ -29,15 +29,15 @@ spec · `docs/product-spec/03-pluggable-seams-and-extension-model.md:25-25` · high · sha:0adae2d6a47f - Parametric deserialization targets MUST be expressible through a full generic type capture (SEAM-21). spec · `docs/product-spec/03-pluggable-seams-and-extension-model.md:25-25` · high · sha:0adae2d6a47f -- Provider resolution MUST follow a fixed precedence: an explicitly installed provider always wins; otherwise the runtime auto-discovers providers registered on the classpath/plugin registry (SEAM-5). +- Provider resolution MUST follow a fixed precedence: an explicitly installed provider always wins; otherwise the runtime auto-discovers providers registered on the classpath/plugin registry (SEAM-5 / IO-33). spec · `docs/product-spec/03-pluggable-seams-and-extension-model.md:34-34` · high · sha:0adae2d6a47f - Resolution MUST throw a descriptive error naming the install hint when zero providers are discoverable, and a descriptive error listing all candidates when more than one distinct provider is discoverable; exactly one discoverable provider is selected silently (SEAM-5). spec · `docs/product-spec/03-pluggable-seams-and-extension-model.md:34-34` · high · sha:0adae2d6a47f -- Explicit installation MUST be idempotent for the same instance and MUST reject installing a different provider when one is already installed, naming both in the error (SEAM-6). +- Explicit installation MUST be idempotent for the same instance and MUST reject installing a different provider when one is already installed, naming both in the error (SEAM-6 / IO-32). spec · `docs/product-spec/03-pluggable-seams-and-extension-model.md:35-35` · high · sha:0adae2d6a47f -- A successful auto-resolution MUST be cached process-wide (SEAM-7). +- A successful auto-resolution MUST be cached process-wide (SEAM-7 / IO-34). spec · `docs/product-spec/03-pluggable-seams-and-extension-model.md:36-36` · high · sha:0adae2d6a47f -- When an explicit install replaces a different provider that had already been auto-resolved and handed out, the runtime SHOULD emit a warning rather than fail, because objects may already exist against the previous provider (SEAM-8). +- When an explicit install replaces a different provider that had already been auto-resolved and handed out, the runtime SHOULD emit a warning rather than fail, because objects may already exist against the previous provider (SEAM-8 / IO-35). spec · `docs/product-spec/03-pluggable-seams-and-extension-model.md:38-38` · high · sha:0adae2d6a47f - The registry SHOULD tolerate one logical provider seen through more than one loader without misreporting it as multiple, de-duplicating by concrete implementation identity, and SHOULD recognize a thin delegating shim as its canonical target (SEAM-10). spec · `docs/product-spec/03-pluggable-seams-and-extension-model.md:39-39` · high · sha:0adae2d6a47f diff --git a/docs/knowledge/serde.md b/docs/knowledge/serde.md index b3fd959..b47ac6c 100644 --- a/docs/knowledge/serde.md +++ b/docs/knowledge/serde.md @@ -5,9 +5,9 @@ spec · `docs/product-spec/14-serialization-serde.md:7-7` · high · sha:c6bc7789c3a9 - A Serde MUST declare the wire media type it produces, that media type MUST be used as the default Content-Type when a request body is created from a value plus a Serde, and the media type MUST NOT be defaulted to a format-agnostic constant at the SPI level. spec · `docs/product-spec/14-serialization-serde.md:8-8` · high · sha:c6bc7789c3a9 -- When encoding into or decoding from a caller-supplied stream, the serializer/deserializer MUST read/write the payload fully to EOF on the read side but MUST NOT close or take ownership of the caller's stream, and the encode-into-buffer profile likewise touches only the target region without assuming ownership. +- When encoding into or decoding from a caller-supplied stream, the serializer/deserializer MUST read/write the payload fully to EOF on the read side but MUST NOT close or take ownership of the caller's stream, and the encode-into-buffer profile likewise touches only the target region without assuming ownership (SEAM-20). spec · `docs/product-spec/14-serialization-serde.md:12-12` · high · sha:c6bc7789c3a9 -- The encode-into-buffer serialization profile MUST return the number of bytes written, MUST honor a start offset, MUST throw a range/overflow error distinct from the serde exception type when the offset is out of range or the payload does not fit, and MUST leave bytes before the offset untouched. +- The encode-into-buffer serialization profile MUST return the number of bytes written, MUST honor a start offset, MUST throw a range/overflow error distinct from the serde exception type when the offset is out of range or the payload does not fit, and MUST leave bytes before the offset untouched (SEAM-20). spec · `docs/product-spec/14-serialization-serde.md:13-13` · high · sha:c6bc7789c3a9 - Every decode operation MUST take an explicit runtime type witness for the target type, and a decoder MUST NOT rely on erased compile-time generics because on an erasure-based runtime that silently yields an untyped map/list which detonates as a cast error on first field access. spec · `docs/product-spec/14-serialization-serde.md:17-17` · high · sha:c6bc7789c3a9 @@ -15,11 +15,11 @@ spec · `docs/product-spec/14-serialization-serde.md:18-18` · high · sha:c6bc7789c3a9 - An ergonomic reified/inline decode helper, where the host language offers one, MUST capture the full generic type and route through the generic carrier rather than forwarding only the raw class. spec · `docs/product-spec/14-serialization-serde.md:19-19` · high · sha:c6bc7789c3a9 -- The generic type carrier MUST capture a concrete, fully-resolved type at construction and MUST reject construction with no type argument or an unresolved type variable, failing fast with an actionable message. +- The generic type carrier MUST capture a concrete, fully-resolved type at construction and MUST reject construction with no type argument or an unresolved type variable, failing fast with an actionable message (SEAM-22). spec · `docs/product-spec/14-serialization-serde.md:20-20` · high · sha:c6bc7789c3a9 -- Encode/decode failures MUST surface as the SDK's stable serde exception type or a subtype; adapters MUST catch the backing codec's processing failures and rethrow as the serde type, MUST chain the original as the cause, and MUST NOT allow a backing-library exception type to escape the SPI. +- Encode/decode failures MUST surface as the SDK's stable serde exception type or a subtype; adapters MUST catch the backing codec's processing failures and rethrow as the serde type, MUST chain the original as the cause, and MUST NOT allow a backing-library exception type to escape the SPI (SEAM-23). spec · `docs/product-spec/14-serialization-serde.md:24-24` · high · sha:c6bc7789c3a9 -- Write-path serde failures MUST be a serialization-specific subtype and read-path failures a deserialization-specific subtype, both of a common root exception type, so callers can distinguish direction while catching one base type. +- Write-path serde failures MUST be a serialization-specific subtype and read-path failures a deserialization-specific subtype, both of a common root exception type, so callers can distinguish direction while catching one base type (SEAM-23). spec · `docs/product-spec/14-serialization-serde.md:25-25` · high · sha:c6bc7789c3a9 - A genuine stream I/O error raised while reading/writing a caller-owned stream MUST propagate unwrapped as an I/O error and MUST NOT be re-wrapped as a serde exception; only malformed-input, shape-mismatch, or unencodable-value failures are wrapped. spec · `docs/product-spec/14-serialization-serde.md:27-27` · high · sha:c6bc7789c3a9 diff --git a/docs/knowledge/sse-streaming.md b/docs/knowledge/sse-streaming.md index 54df65b..d833c9c 100644 --- a/docs/knowledge/sse-streaming.md +++ b/docs/knowledge/sse-streaming.md @@ -41,9 +41,9 @@ spec · `docs/product-spec/13-server-sent-events-and-streaming.md:37-37` · high · sha:dd401a407f5d - The SSE streaming facade MUST own exactly one closeable resource and MUST close it exactly once across the stream's whole life regardless of termination path (clean end, explicit close, use-block exit, partial consume, or mid-stream failure). spec · `docs/product-spec/13-server-sent-events-and-streaming.md:45-45` · high · sha:dd401a407f5d -- On reader end-of-stream during iteration, the SSE facade MUST both terminate the iterator cleanly and release the resource, so a fully-consumed stream needs no explicit close. +- On reader end-of-stream during iteration, the SSE facade MUST both terminate the iterator cleanly and release the resource, so a fully-consumed stream needs no explicit close (SSE-24). spec · `docs/product-spec/13-server-sent-events-and-streaming.md:46-46` · high · sha:dd401a407f5d -- A partial consume of the SSE stream MUST NOT strand the resource; closing after reading only some events MUST release it. +- A partial consume of the SSE stream MUST NOT strand the resource; closing after reading only some events MUST release it (SSE-25). spec · `docs/product-spec/13-server-sent-events-and-streaming.md:47-47` · high · sha:dd401a407f5d - The SSE streaming facade MUST be single-pass such that obtaining an iterator succeeds at most once, and a second attempt MUST fail loudly. spec · `docs/product-spec/13-server-sent-events-and-streaming.md:48-48` · high · sha:dd401a407f5d