Engineering-Specs/Validation/Verification-Report-V1.md
Crawler Factory — Engineering Specs Verification Report
Reviewer: Senior Code Reviewer (Opus 4.7, 1M context)
Date: 2026-04-30
Specs reviewed: 13 cluster docs (~22,040 lines) at Projects/Crawler-Factory/Engineering-Specs/
Verdict: NOT ready — multiple Critical interface mismatches will break the build before Spec 13 smoke can run. Specs 02, 04, 05, 07, 08, 11, 12 all need targeted edits before the Sonnet 4.6 build pipeline starts.
Strengths (acknowledge first)
- Spec 01 is canonical-grade. DSS registry, zod record union, sharded session store, blueprint store — the schemas, validation, logger conventions, try/catch discipline, and TDD cadence all line up with CodingSOPs/TestingSOPs verbatim. Every other spec correctly bases on this foundation.
- Spec 02 (sitemap discovery) is empirically grounded: depth-3 nesting test (Microsoft pattern), 50K-URL urlset test (sitemap-protocol max), gzip via both
.xml.gzURL suffix andContent-Encoding, hreflang dedupe, cycle prevention. The streamingasync function*boundary is preserved end-to-end and theScopeEstimateSchemazod boundary is clean. - Spec 03 (WAF + sampling) correctly translates §19b L0–L3 into the
probeWafladder, picks a defensible singleton-browser pattern for Playwright, and keepsSamplelocal tosampler.ts(per user directive — not promoted totypes.ts). The 5MB cap withBuffer.byteLength/Buffer.from('utf8').subarrayis byte-correct (UTF-16 string length would be wrong). - Spec 04 cascade aggregator correctly implements §19c — max-confidence + agreement-boost (0.05 × n−1, capped at 0.95) + LLM gate at <0.55 + traffic-light bands. The 8-layer test matrix is comprehensive (CMS, URL, JSON-LD, OG, microdata, date+author, semantic HTML, LLM).
- Spec 06 IndexManager correctly defers neuralSearch (
neuralPending: true) on cold-start indices — the Algolia API rejects neural before Insights events are present (master plan §10 risk row). Drift detection onsearchableAttributes/customRanking/attributesForFacetingis idempotent and correct. - SOP rules section is present in every spec (Spec 01 §"SOP rules applied" through Spec 13). The Python→TypeScript translation (Pydantic→zod, pyright→tsc, Ruff→eslint+prettier, Pino logger via
lib/utils/logger.ts) is consistent across the 13 documents. - Master plan coverage is broad: all 9 §1 locked decisions are surfaced; §3a–§3d (sitemap, classification, index, WAF) all have spec homes; the 6 NEW §20a tasks (CMS detector, WAF/Playwright, site-type rubric, canonical inferer, scope estimator, brand-domain discoverer) all have explicit specs (04, 03, 04, 05, 02, 07).
Issues by axis
Axis A — Plan coverage
A1 (Important) — record_type for content records contradicts master plan §4
Spec 05 (record-extractor-template.ts) — every per-domain template emits records with record_type: 'enterprise_ledger' AND content_domain: 'marketing'. Per the master plan §4 / §4c the new factory writes per-domain records (MarketingRecord, SupportRecord, …) to per-domain indices (algoliacentral_marketing, etc.). The legacy enterprise_ledger index has its OWN CrawlerRecord schema (Python, packages/crawl/src/crawl/models.py) and is explicitly NOT a target of the factory.
Spec 01's RecordBaseSchema (line 566 of 01-foundations.md) does NOT include a record_type field. The discriminator is content_domain (a literal per record type). record_type is reserved for the session-storage sharding inside algoliacentral_factory_sessions ('session' | 'url_shard' | 'sample' | 'log').
Effect at build time: zod's default object-mode strips unknown keys, so the validator passes — but every record carries a misleading record_type: 'enterprise_ledger' field that's silently dropped. Misleading. Also future readers will assume the factory writes to the legacy index.
Fix: Edit Spec 05 Task 2 templates — remove the record_type: 'enterprise_ledger' line from all 9 per-domain templates. Update the Spec 05 __tests__/record-extractor-template.test.ts checks (expect(tpl).toContain("record_type: 'enterprise_ledger'")) to expect(tpl).not.toContain('enterprise_ledger'). Update the Spec 05 sandbox-runner test (line 1376–1389) to drop the same field.
File:line references:
- Wrong: Engineering-Specs/05-extraction-sandbox.md:666, 695, 722, 751, 780, 817, 848, 877, 902 (one per domain template)
- Wrong: Engineering-Specs/05-extraction-sandbox.md:567–574 (test asserts the wrong invariant)
- Right reference: Engineering-Specs/01-foundations.md:566–577 (RecordBaseSchema — no record_type field)
- Master plan: 00-Plan.md:323–376 (record schemas), 00-Plan.md:377 (legacy ledger NOT touched)
A2 (Minor) — bedrock CMS not in master plan
Spec 04 adds bedrock to CmsIdEnum (Mozilla-specific, mzp-* class). Master plan §3b lists 11 CMSes and does not mention bedrock. Adding it is a beneficial deviation (Mozilla.org is a real audit target) but should be noted in §20a or the Spec 04 rationale.
File: Engineering-Specs/04-cms-classification.md:168 (CmsIdEnum), §1.3 detector branch.
A3 (Minor) — mediawiki is in CmsIdEnum but wikipedia-mediawiki is in SiteTypeEnum
These are deliberately split (CMS != site-type) but the naming inconsistency between mediawiki and wikipedia-mediawiki will trip up future readers. Consider aligning to one form.
Files: Engineering-Specs/04-cms-classification.md:166, 224.
A4 (Minor) — §16f auto-OEM playbook not explicitly tested
Specs claim coverage of all §16 site types via the rubric, but no spec includes a fixture/test for §16f auto manufacturer (BMW/Mercedes localization + AEM patterns). The rubric's aem-enterprise row covers it implicitly. Acceptable for v1.
Axis B — Cross-spec interface compatibility
These are the build-breaking issues. Most must be fixed before Sonnet 4.6 starts.
B1 (Critical) — Spec 08 create-crawler.ts calls CrawlerClient.createCrawler with the wrong shape
Spec 06 declares:
interface CreateCrawlerInput { name: string; config: Record<string, unknown>; }
async createCrawler(input: CreateCrawlerInput): Promise<{ crawlerId: string }>
Spec 08 (api-src/factory/create-crawler.ts, line 2389–2392) calls:
const crawlerId = await crawler.createCrawler({
name: crawlerName,
indexName: dssEntry.indexName, // ← unknown property; should be inside config{}
});
Two problems:
1. indexName is not a key on CreateCrawlerInput. tsc rejects.
2. The return type is { crawlerId: string }, but Spec 08 assigns it to a variable named crawlerId and immediately passes it as the first arg to patchConfig(crawlerId, …) and startReindex(crawlerId). Those expect a string, not an object. tsc catches this; runtime would 4xx.
Fix: In Spec 08 Task 6 implementation, change to:
const { crawlerId } = await crawler.createCrawler({
name: crawlerName,
config: { indexName: dssEntry.indexName /* + other config block */ },
});
Or update Spec 06's CreateCrawlerInput to surface indexName at the top level if Algolia's actual API expects it there (verify against packages/crawl/src/crawl/manage_seeder.py:34-102 first).
File:line:
- Wrong: Engineering-Specs/08-backend-api.md:2389-2392, 2405
- Contract: Engineering-Specs/06-crawler-client-index-manager.md:146-151, 180-186
B2 (Critical) — startReindex return-shape mismatch (taskID vs taskId)
Spec 08 destructures const { taskID: reindexTaskId } = await crawler.startReindex(crawlerId); (line 2405).
Spec 06 declares TaskResponseSchema = z.object({ taskId: z.string().min(1) }) (line 134) — lowercase taskId. The destructuring { taskID: reindexTaskId } would bind reindexTaskId = undefined because no field taskID exists. zod parse would have already failed earlier; if not, the blueprint write proceeds with reindexTaskId = undefined, breaking session persistence.
Fix: Pick one casing. Algolia Crawler API uses taskID (uppercase D in the Python reference, manage_seeder.py:69-76 — verify). Update Spec 06's TaskResponseSchema to taskID: z.string(). Or update Spec 08's destructuring to { taskId }.
File:line:
- Wrong: Engineering-Specs/08-backend-api.md:2405
- Contract: Engineering-Specs/06-crawler-client-index-manager.md:133-136
B3 (Critical) — Spec 08 sample.ts and discover.ts call sampleUrls(urls, n) instead of sampleUrls(urls, opts)
Spec 03 declares sampleUrls(urls: string[], opts: SampleUrlsOpts = {}) where SampleUrlsOpts = { perUrlTimeoutMs, maxBytes, concurrency, userAgent }. There is NO numeric "max samples" parameter — the function returns one Sample per input URL.
Spec 08 (line 604, 1040) calls:
const samples = await sampleUrls(group.sampleUrls, SAMPLES_PER_GROUP); // ← number, not opts
const samples = await sampleUrls(group.sampleUrls, parsed.n); // ← number, not opts
tsc catches this (number is not assignable to SampleUrlsOpts). Runtime would coerce silently with TypeScript as cast and behave incorrectly.
Fix: Either (a) Spec 08 slices group.sampleUrls.slice(0, SAMPLES_PER_GROUP) before calling, OR (b) Spec 03's SampleUrlsOpts adds maxSamples?: number and the sampler honors it.
File:line:
- Wrong: Engineering-Specs/08-backend-api.md:604, 1040
- Contract: Engineering-Specs/03-waf-sampling.md:1049-1063
B4 (Critical) — Spec 08 addSample uses non-existent Sample.urlHash
Spec 03 Sample type has { url, html, status, contentLength, isSpa, headers, transport, truncated }. There is NO urlHash field.
Spec 08 (line 605–610, 1042–1046):
await store.addSample(sessionId, group.id, s.urlHash, { url, html, status });
s.urlHash is undefined. Spec 01's addSample(sessionId, pathGroupId, urlHash, sample) would write objectID: ${sessionId}:sample:undefined, colliding for every URL.
Fix: Either Spec 03's Sample exposes urlHash (computed from url), or Spec 08's discover/sample handlers compute it inline (e.g. simple djb2 or sha1 of s.url).
File:line:
- Wrong: Engineering-Specs/08-backend-api.md:605, 1042
- Contract: Engineering-Specs/03-waf-sampling.md:1037-1047
B5 (Critical) — Spec 08 classify(...) signature does not exist in Spec 04
Spec 04 declares:
async function classify(html: string, url: string, opts: ClassifyOptions): Promise<Verdict>
// returns { contentDomain, confidence, detectedVia, contributingLayers, schemaOrgType }
Spec 08 (line 613) calls:
const classification = await classify({ pathGroup: group, samples });
emit({
type: 'classified',
pathGroupId: group.id,
contentDomain: classification.contentDomain,
confidence: classification.confidence,
method: classification.method, // ← Verdict has no `.method`
});
Two errors:
1. The argument shape { pathGroup, samples } doesn't match (html, url, opts).
2. classification.method doesn't exist on Verdict — the field is detectedVia.
Fix: Either (a) Spec 04 adds a higher-level classifyPathGroup(pathGroup, samples) that internally iterates samples and runs classify(...) per page, then reduces to a single verdict; or (b) Spec 08 inlines that loop over samples and passes (sample.html, sample.url, { cms }) per sample.
For SSE event compatibility, also resolve method vs detectedVia. Master plan §4g explicitly names the SSE field method. Either:
- Spec 04 Verdict adds an alias method (matching detectedVia), or
- Spec 08 maps detectedVia → method at SSE emit time.
File:line:
- Wrong: Engineering-Specs/08-backend-api.md:613-619
- Contract: Engineering-Specs/04-cms-classification.md:201-210, 1555-1577
- Master plan: 00-Plan.md:471-475
B6 (Critical) — Spec 08 analyzeStructure and generateExtractor signatures don't match Spec 05
Spec 05 declares:
analyzeStructure(sample: StructureSample, contentDomain: ContentDomain, opts?: AnalyzeOpts)
generateExtractor(opts: { pathGroup, structureAnalysis, dssEntry, contentDomain, llm, userFeedback? })
Spec 08 (line 1361-1368, generate-extractor.ts) calls:
const analysis = await analyzeStructure({
sessionId: parsed.sessionId,
pathGroupId: parsed.pathGroupId,
contentDomain: group.detectedDomain,
});
const generated = await generateExtractor({
contentDomain: group.detectedDomain,
dssEntry,
analysis, // ← key is `structureAnalysis`, not `analysis`
userFeedback: parsed.userFeedback,
// missing required `pathGroup` and `llm`
});
analyzeStructure is being called with a session-context object instead of a StructureSample ({url, html, status, …}). generateExtractor is missing pathGroup (required for pathsToMatch derivation) and llm (required to fill placeholders — Spec 05's LlmFn parameter).
Fix: In Spec 08 generate-extractor.ts:
1. Load samples from the session (not just IDs) — call store to fetch the persisted record_type='sample' records.
2. Pick one (typically the first); pass to analyzeStructure(sample, group.detectedDomain, { llm: llmAdapter }).
3. Then call generateExtractor({ pathGroup: group, structureAnalysis: analysis, dssEntry, contentDomain: group.detectedDomain, llm: llmAdapter, userFeedback }).
4. Wire an LlmFn adapter from lib/llm/index.ts's getLLMProvider().generateContent(...) into a (prompt: string) => Promise<string> shape.
File:line:
- Wrong: Engineering-Specs/08-backend-api.md:1361-1370
- Contract: Engineering-Specs/05-extraction-sandbox.md:356-360, 1170-1186
B7 (Critical) — Spec 11 CategoryConfiguratorProps requires 7 props; Spec 12 passes 3
Spec 11 declares (line 1356-1367):
interface CategoryConfiguratorProps {
sessionId: string;
pathGroupId: string;
pathGroup: PathGroup;
contentDomain: ContentDomain;
open: boolean;
onClose: () => void;
onCommit: (payload: { recordExtractor: string; crawlerConfig: CrawlerConfig }) => void;
}
Spec 12 (line 652-657) renders:
<CategoryConfigurator
pathGroupId={props.activePathGroupId}
onCommit={props.onCommitDrawer}
onCancel={props.onCommitDrawer} // ← prop name `onCancel` not declared
/>
Missing: sessionId, pathGroup, contentDomain, open, onClose. Mismatched: onCancel vs onClose. tsc rejects.
Fix: Pick one props contract and update both. Recommended: Spec 11 is the implementation truth; Spec 12 must pass all 7 props (and consume pathGroup/contentDomain from props.session.pathGroups[i] rather than just the pathGroupId). Also rename onCancel → onClose in Spec 12, OR add onCancel to Spec 11's props interface.
File:line:
- Wrong: Engineering-Specs/12-frontend-orchestrator.md:652-657
- Contract: Engineering-Specs/11-frontend-configurator.md:1356-1367
B8 (Important) — Spec 07 references SiteType 'multi-brand-federated'; Spec 04 enum is 'multi-brand-corporate'
Spec 04 SiteTypeEnum (line 215-228) defines 'multi-brand-corporate'. Spec 07 (line 14) says it gates on 'multi-brand-federated'. tsc would reject any consumer trying to compare to 'multi-brand-federated'.
Fix: Use 'multi-brand-corporate' everywhere — update Spec 07 lines 7, 14, and any test fixture/predicate to match Spec 04.
File:line:
- Wrong: Engineering-Specs/07-brand-discovery.md:7, 14
- Contract: Engineering-Specs/04-cms-classification.md:215-228
B9 (Important) — Spec 07 claims detectSiteType returns { siteType, confidence }; Spec 04 returns plain SiteType
Spec 04 (line 1897): function detectSiteType(input: RubricInput): SiteType — returns the enum value directly, no confidence wrapper.
Spec 07 (line 14) describes Spec 04 as exposing detectSiteType(sample): { siteType, confidence }. If Spec 07's implementation actually consumes a .confidence field, tsc catches it.
Fix: Update Spec 07's prose to match Spec 04's actual signature (just SiteType), OR — if confidence is genuinely needed for Spec 07's gating logic — extend Spec 04's detectSiteType to return { siteType, confidence } AND update every other consumer (none today, but Spec 12 may surface it later).
File:line:
- Wrong: Engineering-Specs/07-brand-discovery.md:14
- Contract: Engineering-Specs/04-cms-classification.md:1897-1905
B10 (Important) — Spec 11 Sample.fetchedVia vs Spec 03 Sample.transport
Spec 11 defines its own UI-local Sample (line 76-82) with fetchedVia: 'fetch' | 'playwright'. Spec 03's runtime Sample.transport: 'fetch' | 'playwright'. The API endpoint in Spec 08 (sample.ts) returns Spec 03's shape but the UI expects Spec 11's. Neither spec maps between them.
Effect: Either Spec 08's sample endpoint must rename the field at the JSON boundary ({ ...s, fetchedVia: s.transport }), or Spec 11 must rename to transport. Today, the field will be transport on the wire and the UI reading s.fetchedVia gets undefined.
Fix: Cleanest: rename Spec 11's local fetchedVia → transport to match the wire format. Document the implicit "UI types mirror Sample subset" intent in Spec 11's types.ts header.
File:line:
- Wrong: Engineering-Specs/11-frontend-configurator.md:80
- Contract: Engineering-Specs/03-waf-sampling.md:1044
B11 (Important) — Spec 11 StructureAnalysis shape != Spec 05 StructureAnalysis shape
Spec 05 returns { schemaOrgJsonLd, selectors, missing, confidence }. Spec 11 expects { detectedSelectors[], jsonLd, microdata, cmsSignature }.
These are intentionally local UI types per Spec 11's design ("UI concerns local"), but Spec 08's generate-extractor endpoint returns { recordExtractor, crawlerConfig } only — the analysis is persisted on the session blueprint, not returned. Spec 11 fetches and reads analysis from … where? The drawer code path needs explicit clarification.
Fix: Either (a) Spec 08 generate-extractor returns the analysis alongside the extractor ({ recordExtractor, crawlerConfig, analysis: <UI-shaped> }), with explicit field-mapping from Spec 05's shape to Spec 11's shape; or (b) the UI fetches a different endpoint for the structure summary; or (c) Spec 11's StructureAnalysis is dropped in favor of a thin selector array and the rest comes from JSON-LD passthrough.
File:line:
- Engineering-Specs/11-frontend-configurator.md:84-91
- Engineering-Specs/05-extraction-sandbox.md:261-267
B12 (Minor) — Spec 02 "Out of scope" mentions Spec 06/07 owning discover endpoint; Spec 08 actually owns it
Spec 02 line 1572-1573 says "Persistence of discovered URLs into url_shard records → Spec 06 / Spec 07 (/api/factory/discover endpoint…)". The endpoint is in Spec 08. Doc drift only — no build impact.
B13 (Minor) — Spec 03 "Out of scope" mentions Spec 06 wiring sampleUrls into SessionStore.addSample
Spec 03 line 1606. Spec 08 (not 06) is the wiring point. Doc drift only.
Axis C — SOP compliance per spec
C1 (Important) — Spec 04 classifier.ts has imports interleaved with code
Spec 04 Step 2.3 creates classifier.ts with imports at top. Step 3.3 says "Append" — and the appended block (line 1477-1479) contains additional import { z } from 'zod', import { getLLMProvider } from '../llm', import { listContentDomains, DSS } from './dss' mid-file:
// ─── Layer 8: LLM tiebreaker ────────────────────────────────────────────────
import { z } from 'zod'; // ← mid-file imports
import { getLLMProvider } from '../llm';
import { listContentDomains, DSS } from './dss';
const LLM_RESPONSE_SCHEMA = z.object({ ... });
ESM hoists imports, so most build tools tolerate this — but it violates style and CodingSOPs §9 ("standard formatting"). A diligent agent following the spec literally produces unidiomatic code; a linting agent may auto-move them silently and the resulting diff is harder to review.
Fix: Update Spec 04 Step 2.3 to seed all required imports at the top of classifier.ts from the start:
import * as cheerio from 'cheerio';
import { z } from 'zod';
import { createLogger } from '../utils/logger';
import { dssBySchemaOrgType, listContentDomains, DSS, type ContentDomain } from './dss';
import { getLLMProvider } from '../llm';
import { type CmsId, type Verdict } from './types';
Then Step 3.3 only appends declarations (no imports).
File:line: Engineering-Specs/04-cms-classification.md:1477-1479
C2 (Minor) — Spec 05 sandbox-runner uses eslint-disable-next-line no-new-func
Acceptable — new Function() IS the sandbox primitive. But add a WHY comment ("we control the source — extractor was generated by us, not user input") right above. CodingSOPs §8 says comments must explain WHY.
File: Engineering-Specs/05-extraction-sandbox.md:1635-1636
C3 (Minor) — record_type strings are stripped silently by zod default mode
Spec 05's templates emit record_type: 'enterprise_ledger'. Spec 01's record schemas don't declare .strict() — zod silently drops unknown keys. Combined with A1, this means a wrong field passes validation. Either:
- Add .strict() to all 9 record schemas in Spec 01 — would surface A1 as a failing test.
- Or accept silent stripping and document why.
Recommend: not adding .strict() (downstream callers may legitimately add fields), but DEFINITELY remove the wrong field per A1.
Axis D — Research incorporation
D1 (Pass) — CMS-FIRST cascade verified
Spec 04 Step 2.3 implements the cascade with classifyByCms as Layer 1, classifyByUrl as Layer 2, JSON-LD as Layer 3. Aggregator's LLM gate at <0.55 is correct. Confidence bands per §19c (≥0.85 green, 0.65–0.84 yellow, <0.65 amber) are tested in Step 3.1.
D2 (Pass) — Sitemap walker has no caps; tested at depth-3 + 50K
Spec 02 Step 2.5 (sitemapindex recursion), Step 2.7 (cycle prevention), Step 3.5 (depth-3 Microsoft pattern), Step 3.7 (50K-URL urlset). All match §3a, §14b, §16l.
D3 (Pass) — Playwright is in v1
Spec 03 Task 0 promotes playwright to runtime dep + bundler externals; Task 2 implements the singleton fetcher. Matches §3d, §6 phasing, §20a Task B.
D4 (Pass) — Site-type rubric has 13 ordered predicates
Spec 04 Task 4 implements the rubric per §19d, first-match-wins, with 'custom' fallback as the 13th. The Drupal-class-wins-over-WordPress test (line 1813-1819) confirms ordering.
D5 (Pass) — Product-catalog template is DOM-extraction-first per §16o
Spec 05 Step 2.1 explicitly tests "DOM selectors come BEFORE JSON-LD attempt for product-catalog" (line 597). The template body confirms — name/description/price/image/variants selectors fire first; JSON-LD enriches afterward.
D6 (Pass) — Brand-discoverer reads from §14f-confirmed paths
Spec 07 includes /our-brands, /portfolio, /companies, /family-of-brands per §16k. Diageo fixture matches the empirical findings.
D7 (Partial) — §9 verification protocol coverage
Spec 13 covers most of the §9 16-step protocol but does not explicitly test step 14 (hard reload → session resumes via ?session=<id>). Step 4.5 of Spec 13 references "session resume" but doesn't make it an observation gate. Recommend: add explicit reload step.
File: Engineering-Specs/13-bundle-smoke-docs.md Task 4.
Axis E — Hallucinated dependencies
E1 (Critical) — Multiple cross-spec calls invoke functions whose actual signatures don't exist
Issues B1, B3, B4, B5, B6 are all instances of "Spec 08 calls a lib/factory/* function that does NOT exist with that signature in the spec that owns it". Because Spec 08 was authored by a parallel agent that didn't read Spec 03/04/05/06 verbatim, the integration glue is fictional. tsc catches these at build time.
E2 (Critical) — Spec 11 references types that the rest of the system doesn't expose
Spec 11's Sample, StructureAnalysis, CrawlerConfig are local UI types that diverge from their backend counterparts (B10, B11). The "API → UI mapper" is implicit — no spec owns it. Either Spec 08's response handler renames fields, or Spec 11 must explicitly map at fetch time. The current specs do neither.
E3 (Pass) — getLLMProvider, LLMResponse, LLMGenerateRequest exist
Verified at lib/llm/index.ts:19 and lib/llm/types.ts:8-17. Spec 04's mock and Step 3.3 implementation both use the contract correctly (generateContent({ systemPrompt, messages, jsonMode, temperature }) + resp.json()).
E4 (Pass) — cheerio, algoliasearch, playwright, fast-xml-parser, zod all real
Each is either in the existing repo (cheerio, algoliasearch, playwright, zod) or installed in Spec 01 (fast-xml-parser) / promoted in Spec 03 (playwright dependency tier).
E5 (Pass) — createLogger from lib/utils/logger.ts exists with the documented signature
Verified at lib/utils/logger.ts:67.
Recommendations
Must fix before Sonnet 4.6 build (Critical-only — blocks build)
- Spec 05 templates: remove
record_type: 'enterprise_ledger'from all 9 templates (A1). - Spec 08 create-crawler.ts: align with Spec 06's
CrawlerClientAPI — fixcreateCrawlerarg shape and return-shape destructuring (B1, B2). - Spec 08 sample.ts + discover.ts: align
sampleUrls(urls, opts)calls +Sample.urlHashaccess (B3, B4). - Spec 08 discover.ts: rewire
classify(html, url, {cms})per-sample with reduce; mapdetectedVia → methodfor SSE (B5). - Spec 08 generate-extractor.ts: align
analyzeStructure+generateExtractorsignatures with Spec 05 — load samples first, inject an LLM adapter (B6). - Spec 12 orchestrator: pass all 7
CategoryConfiguratorPropsOR extend Spec 11's interface to match what Spec 12 wants to pass (B7). - Spec 07: rename
'multi-brand-federated'→'multi-brand-corporate'(B8) AND drop.confidenceaccess ondetectSiteTypereturn (B9).
Should fix before build starts (Important — broken feature even if it compiles)
- Spec 11 → Spec 03 field rename:
Sample.fetchedVia→Sample.transport(B10). - Spec 11 ↔ Spec 05
StructureAnalysisshape unification OR explicit mapper in Spec 08 generate-extractor handler (B11). - Spec 04 classifier.ts: hoist all imports to top of file in Step 2.3 (C1).
Nice to have (Minor)
- Spec 02/03 "Out of scope" doc fixes (B12, B13).
- Spec 04
bedrockrationale +mediawiki/wikipedia-mediawikinaming alignment (A2, A3). - Spec 13: explicit session-resume observation gate (D7).
- Spec 05: document
record_typedecision in CHANGELOG-style note (C3).
Process recommendation
The build pipeline is parallelizable per the dependency graph (clusters 02/03/06/07 after 01, then 04, then 05, then 08, then 09–12, then 13). Critical Issue #2–#6 all live in Spec 08. A single editing pass over Spec 08 can resolve five of the seven blockers because Spec 08 is the integration glue layer where parallel-author drift is highest.
Recommend: have ONE agent re-read Specs 03, 04, 05, 06 verbatim and rewrite Spec 08's orchestrator code blocks to match the actual exports. Then have a second agent re-read Spec 11 to fix B7 in Spec 12. Then have a third agent fix Specs 05 (A1 templates) and 07 (B8/B9).
Final assessment
Verdict: NOT ready.
The plan-coverage and architecture work is good — every §16 site type, every §19 protocol element, every §20a NEW task has a spec home. The schemas, logger discipline, TDD cadence, and SOP rules sections are all rigorous.
But seven Critical issues (six interface mismatches in Spec 08 + one prop mismatch in Spec 12 + one enum drift in Spec 07) plus the misleading record_type field in Spec 05 templates will break the Sonnet 4.6 build before Spec 13's smoke test can even run. tsc will catch most of them; the rest will surface as 4xx responses or silently undefined fields the moment the discover endpoint runs.
Path to "Ready with fixes": apply the 7-step "Must fix" list above. Each is a localized edit (no cascading changes). Estimated effort: a single agent in a single session, ~1 hour, no new tests required (the existing tests already exercise the mismatched code paths and will turn red, then green, naturally).
After those fixes, run a quick second-pass review against this report's B-axis checklist before kicking off the parallel build.
File:line reference summary (one-stop fix index)
| ID | Severity | Spec | Line(s) | Mismatched against |
|---|---|---|---|---|
| A1 | Important | 05 | 666, 695, 722, 751, 780, 817, 848, 877, 902, 567-574 | 01:566-577, 00-Plan §4 |
| B1 | Critical | 08 | 2389-2392, 2405 | 06:146-151, 180-186 |
| B2 | Critical | 08 | 2405 | 06:133-136 |
| B3 | Critical | 08 | 604, 1040 | 03:1049-1063 |
| B4 | Critical | 08 | 605, 1042 | 03:1037-1047 |
| B5 | Critical | 08 | 613-619 | 04:201-210, 1555-1577; 00-Plan §4g |
| B6 | Critical | 08 | 1361-1370 | 05:356-360, 1170-1186 |
| B7 | Critical | 12 | 652-657 | 11:1356-1367 |
| B8 | Important | 07 | 7, 14 | 04:215-228 |
| B9 | Important | 07 | 14 | 04:1897-1905 |
| B10 | Important | 11 | 80 | 03:1044 |
| B11 | Important | 11+05 | 11:84-91 vs 05:261-267 | (cross-cluster) |
| B12 | Minor | 02 | 1572-1573 | doc drift (08 owns endpoint) |
| B13 | Minor | 03 | 1606 | doc drift (08 owns endpoint) |
| C1 | Important | 04 | 1477-1479 | CodingSOPs §9 (imports at top) |
| C2 | Minor | 05 | 1635-1636 | CodingSOPs §8 (WHY comment) |
| D7 | Minor | 13 | Task 4 | 00-Plan §9 step 14 |