@clear-capabilities/agentic-security-scanner 0.144.0 → 0.147.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +390 -0
- package/bin/agentic-security.js +3813 -83
- package/dist/1122.index.js +702 -0
- package/dist/{301.index.js → 1301.index.js} +2 -2
- package/dist/1379.index.js +591 -0
- package/dist/{444.index.js → 1444.index.js} +13 -4
- package/dist/{660.index.js → 1660.index.js} +2 -2
- package/dist/{700.index.js → 1700.index.js} +2 -2
- package/dist/{905.index.js → 1905.index.js} +3 -3
- package/dist/{920.index.js → 1920.index.js} +3 -3
- package/dist/{238.index.js → 2238.index.js} +3 -3
- package/dist/2271.index.js +165 -0
- package/dist/{985.index.js → 2376.index.js} +1260 -340
- package/dist/2432.index.js +793 -0
- package/dist/2659.index.js +93 -0
- package/dist/{826.index.js → 2826.index.js} +2 -2
- package/dist/{830.index.js → 2830.index.js} +2 -2
- package/dist/2923.index.js +298 -0
- package/dist/{1.index.js → 3001.index.js} +5 -5
- package/dist/{117.index.js → 3117.index.js} +3 -3
- package/dist/3180.index.js +307 -0
- package/dist/3276.index.js +117 -0
- package/dist/{415.index.js → 3415.index.js} +2 -2
- package/dist/{499.index.js → 3499.index.js} +2 -2
- package/dist/3518.index.js +450 -0
- package/dist/{526.index.js → 3526.index.js} +14 -6
- package/dist/{736.index.js → 3736.index.js} +4 -4
- package/dist/{839.index.js → 3839.index.js} +4 -4
- package/dist/{113.index.js → 4113.index.js} +14 -6
- package/dist/{265.index.js → 4265.index.js} +2 -2
- package/dist/{384.index.js → 4384.index.js} +3 -3
- package/dist/4547.index.js +268 -0
- package/dist/4863.index.js +422 -0
- package/dist/{970.index.js → 4970.index.js} +67 -3
- package/dist/5051.index.js +770 -0
- package/dist/{144.index.js → 5144.index.js} +5 -5
- package/dist/{333.index.js → 5333.index.js} +3 -3
- package/dist/5343.index.js +185 -0
- package/dist/5350.index.js +866 -0
- package/dist/5561.index.js +436 -0
- package/dist/{637.index.js → 5637.index.js} +29 -7
- package/dist/{449.index.js → 5830.index.js} +78 -14
- package/dist/6626.index.js +532 -0
- package/dist/6662.index.js +297 -0
- package/dist/{675.index.js → 6675.index.js} +5 -5
- package/dist/{730.index.js → 6730.index.js} +6 -6
- package/dist/6829.index.js +225 -0
- package/dist/6944.index.js +130 -0
- package/dist/{178.index.js → 7178.index.js} +26 -8
- package/dist/{227.index.js → 7227.index.js} +2 -2
- package/dist/7310.index.js +520 -0
- package/dist/{552.index.js → 7552.index.js} +4 -4
- package/dist/7709.index.js +78 -0
- package/dist/8218.index.js +160 -0
- package/dist/{476.index.js → 8476.index.js} +4 -4
- package/dist/{513.index.js → 8513.index.js} +5 -5
- package/dist/{520.index.js → 8520.index.js} +2 -2
- package/dist/{718.index.js → 8718.index.js} +2 -2
- package/dist/{752.index.js → 8752.index.js} +2 -2
- package/dist/8846.index.js +100 -0
- package/dist/{435.index.js → 9091.index.js} +580 -187
- package/dist/{207.index.js → 9207.index.js} +2 -2
- package/dist/{220.index.js → 9220.index.js} +2 -2
- package/dist/9390.index.js +163 -0
- package/dist/{503.index.js → 9503.index.js} +2 -2
- package/dist/{801.index.js → 9801.index.js} +2 -2
- package/dist/{824.index.js → 9824.index.js} +2 -2
- package/dist/agentic-security.mjs +16 -16
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/package.json +23 -10
- package/src/compare.js +6 -1
- package/src/dataflow/CLAUDE.md +2 -2
- package/src/dataflow/catalog.js +42 -0
- package/src/dataflow/orm-write-catalog.js +175 -0
- package/src/engine.js +580 -30
- package/src/fix/apply-fix-service.js +1 -0
- package/src/history-scan.js +22 -5
- package/src/ir/CLAUDE.md +2 -1
- package/src/ir/chrome-probe.mjs +150 -0
- package/src/ir/parser-js.js +94 -7
- package/src/lineage/CLAUDE.md +1203 -0
- package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
- package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
- package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
- package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
- package/src/lineage/DESIGN_REGISTRIES.md +880 -0
- package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
- package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
- package/src/lineage/classification.js +56 -0
- package/src/lineage/coverage.js +658 -0
- package/src/lineage/cross-repo-link.js +107 -0
- package/src/lineage/dataflow-graph.schema.json +184 -0
- package/src/lineage/decision-story.js +206 -0
- package/src/lineage/drift-policy.js +279 -0
- package/src/lineage/driver.js +135 -0
- package/src/lineage/engine.js +992 -0
- package/src/lineage/export-briefing.js +628 -0
- package/src/lineage/export-csv.js +62 -0
- package/src/lineage/export-json.js +238 -0
- package/src/lineage/export-privacy.js +258 -0
- package/src/lineage/federation-loader.js +111 -0
- package/src/lineage/field-identity.js +78 -0
- package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
- package/src/lineage/fixtures/flagship-graph.json +1453 -0
- package/src/lineage/flow-grade.js +221 -0
- package/src/lineage/governance-edit.js +169 -0
- package/src/lineage/graph-builder.js +1114 -0
- package/src/lineage/graph-diff.js +431 -0
- package/src/lineage/graph-snapshot.js +180 -0
- package/src/lineage/handling-analyzer.js +168 -0
- package/src/lineage/ids.js +349 -0
- package/src/lineage/impact-assessment.js +76 -0
- package/src/lineage/impact-engine.js +268 -0
- package/src/lineage/index.js +281 -0
- package/src/lineage/language-coverage-tiers.js +58 -0
- package/src/lineage/obligation-mapping.js +126 -0
- package/src/lineage/obligation-predicates.js +235 -0
- package/src/lineage/observation-adapters.js +282 -0
- package/src/lineage/observation-correlation.js +622 -0
- package/src/lineage/observation-store.js +497 -0
- package/src/lineage/path-query.js +410 -0
- package/src/lineage/path-store.js +400 -0
- package/src/lineage/protection.js +53 -0
- package/src/lineage/recipient-profile.js +192 -0
- package/src/lineage/recipient-registry.js +394 -0
- package/src/lineage/redact-graph.js +224 -0
- package/src/lineage/remediation.js +417 -0
- package/src/lineage/resolve-destination.js +91 -0
- package/src/lineage/runtime-observation.js +464 -0
- package/src/lineage/scenario-diff.js +84 -0
- package/src/lineage/scenario-engine.js +251 -0
- package/src/lineage/scenario.js +101 -0
- package/src/lineage/schema.js +167 -0
- package/src/lineage/sink-registry.js +427 -0
- package/src/lineage/source-registry.js +357 -0
- package/src/lineage/source-seeding.js +212 -0
- package/src/lineage/summaries.js +590 -0
- package/src/lineage/transform-catalog.js +397 -0
- package/src/lineage/transit-protection.js +150 -0
- package/src/lineage/validate.js +285 -0
- package/src/lsp/server.js +49 -2
- package/src/mcp/CLAUDE.md +7 -1
- package/src/mcp/dataflow-tools.js +160 -0
- package/src/mcp/server.js +1 -1
- package/src/mcp/tools.js +22 -1
- package/src/pipeline/assurance-mode.js +64 -1
- package/src/pipeline/finding-schema.js +8 -1
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/CLAUDE.md +140 -0
- package/src/posture/accuracy-scorecard.js +60 -0
- package/src/posture/artifact-registry.js +76 -0
- package/src/posture/auditor-walkthrough.js +192 -13
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/src/posture/compliance-policy.js +12 -2
- package/src/posture/cross-repo-memory.js +7 -2
- package/src/posture/fix-history.js +25 -2
- package/src/posture/fix-verify.js +9 -1
- package/src/posture/fleet.js +0 -0
- package/src/posture/git-history.js +13 -5
- package/src/posture/material-change.js +21 -2
- package/src/posture/mttr.js +75 -12
- package/src/posture/obligation-evidence-pack.js +202 -0
- package/src/posture/pre-incident-archaeology.js +39 -7
- package/src/posture/privacy-framework.js +14 -0
- package/src/posture/provenance/ai-authorship.js +68 -0
- package/src/posture/provenance/branch-entry.js +80 -0
- package/src/posture/provenance/cache.js +143 -0
- package/src/posture/provenance/confidence.js +36 -0
- package/src/posture/provenance/coordinator.js +786 -0
- package/src/posture/provenance/dag-walk.js +249 -0
- package/src/posture/provenance/evidence-attribution.js +59 -0
- package/src/posture/provenance/git-evidence.js +310 -0
- package/src/posture/provenance/lifecycle.js +208 -0
- package/src/posture/provenance/missing-control-resolver.js +137 -0
- package/src/posture/provenance/origin-resolver.js +342 -0
- package/src/posture/provenance/predicate-replay.js +133 -0
- package/src/posture/provenance/providers/config.js +39 -0
- package/src/posture/provenance/providers/github.js +62 -0
- package/src/posture/provenance/providers/gitlab.js +58 -0
- package/src/posture/provenance/repo-lineage.js +74 -0
- package/src/posture/provenance/sca-origin.js +139 -0
- package/src/posture/provenance/schema.js +255 -0
- package/src/posture/provenance/transitive-sca.js +147 -0
- package/src/posture/provenance/validate.js +30 -0
- package/src/posture/provenance-evidence-bundle.js +144 -0
- package/src/posture/remediation-ledger.js +337 -0
- package/src/posture/sbom-diff.js +15 -2
- package/src/posture/secret-history.js +10 -2
- package/src/posture/state-dir.js +38 -14
- package/src/posture/vuln-archaeology.js +8 -2
- package/src/pr-delta.js +25 -4
- package/src/report/index.js +197 -3
- package/src/runScan.js +34 -5
- package/src/sast/rate-limit.js +33 -3
- package/src/server/CLAUDE.md +47 -0
- package/src/server/graph-loader.js +141 -0
- package/src/server/http-server.js +325 -0
- package/src/server/routes.js +129 -0
- package/src/server/security.js +111 -0
- package/src/server/static-assets.js +139 -0
- package/src/util/git-hardening.js +128 -0
- package/dist/11.index.js +0 -353
- package/dist/259.index.js +0 -975
- package/dist/317.index.js +0 -300
- package/dist/609.index.js +0 -741
- package/dist/838.index.js +0 -152
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
# Source/Sink Registries — Reclassification Design Record
|
|
2
|
+
|
|
3
|
+
Scope: Sub-project D of Milestone 1 (see
|
|
4
|
+
`docs/superpowers/plans/2026-08-30-data-flow-explorer-m1-subproject-d-scoping.md`).
|
|
5
|
+
Binding on every later Sub-project D increment, the same way
|
|
6
|
+
`DESIGN_INTRAPROCEDURAL.md` binds Sub-projects A/B and `DESIGN_PATH_PROVENANCE.md`
|
|
7
|
+
binds Sub-project C.
|
|
8
|
+
|
|
9
|
+
This increment (D1) is design + proof-of-concept only. `src/` is unmodified by
|
|
10
|
+
it apart from this file. The mechanical registries (`source-registry.js` = D2,
|
|
11
|
+
`sink-registry.js` = D3) and transformation-kind recognition (D4) are follow-up
|
|
12
|
+
increments; §9 is their checklist.
|
|
13
|
+
|
|
14
|
+
**Every number, list and claim in this document was produced by running real
|
|
15
|
+
code against the live catalogs in the increment that wrote it**, via
|
|
16
|
+
`test/lineage/registry-mapping-poc.test.js`. Nothing here is quoted from the
|
|
17
|
+
scoping doc, and §7.3 records five places where the upstream documents (the
|
|
18
|
+
scoping doc and the D1 task brief) were measurably wrong.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. What this sub-project actually is
|
|
23
|
+
|
|
24
|
+
Sub-projects A/B/C built new analysis capability. Sub-project D builds almost
|
|
25
|
+
none. Its job is to **reclassify** detection the scanner already performs —
|
|
26
|
+
`scanner/src/dataflow/catalog.js`'s 756 entries, proven across the whole
|
|
27
|
+
benchmark corpus — into `schema.js`'s `DataFlowGraph v1` vocabulary.
|
|
28
|
+
|
|
29
|
+
The registries are therefore **pure data consumers**. They read a catalog
|
|
30
|
+
entry's `provenance` / `category` / `vuln` / `label` / `framework` fields and
|
|
31
|
+
emit a `{kind, category, coverageStatus, externality, reason}` decision. They
|
|
32
|
+
**never** re-derive what a call site matches — `matchSource` /
|
|
33
|
+
`matchSinkOrSanitizer` / `matchPrivacySink` already own that, and duplicating
|
|
34
|
+
it would fork a matcher the corpus proves against one that nothing proves.
|
|
35
|
+
|
|
36
|
+
Reading `CATALOG` from `src/lineage/` is data consumption, not a taint-engine
|
|
37
|
+
import, and is consistent with this PRD's isolation principle (§18.1) — the
|
|
38
|
+
same basis on which Sub-project A reuses `access-paths.js`. Confirmed by
|
|
39
|
+
reading `catalog.js`: `CATALOG` is a frozen-by-convention module-level array of
|
|
40
|
+
plain objects with no engine state and no mutation path.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 2. Ground truth (measured, not sampled)
|
|
45
|
+
|
|
46
|
+
| Fact | Value |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `CATALOG.length` | **756** |
|
|
49
|
+
| — `kind: 'source'` | **180** |
|
|
50
|
+
| — `kind: 'sink'` | **194** |
|
|
51
|
+
| — `kind: 'sanitizer'` | **382** |
|
|
52
|
+
| `EXPANDED_SANITIZERS.length` | **324**, and **100% `kind: 'sanitizer'`** |
|
|
53
|
+
| `PRIVACY_SINK_CATALOG.length` | **18** |
|
|
54
|
+
| `PRIVACY_SINK_CATEGORIES.length` | **9** |
|
|
55
|
+
| Distinct source `provenance` values | **12** |
|
|
56
|
+
| Source entries with **no** `provenance` at all | **82** (46% of sources) |
|
|
57
|
+
| Distinct sink `vuln.cwe` values | **20** |
|
|
58
|
+
| Sink entries carrying a `category` field | **0** |
|
|
59
|
+
|
|
60
|
+
Export shapes, confirmed by reading rather than assumed:
|
|
61
|
+
|
|
62
|
+
- `catalog.js` exports `CATALOG` (plus matcher functions and
|
|
63
|
+
`_languageFamilyExtensions`).
|
|
64
|
+
- `catalog-expanded.js` exports `EXPANDED_SANITIZERS` and
|
|
65
|
+
`_expandedSanitizerStats` — **not** a `CATALOG`-shaped array, and it
|
|
66
|
+
contributes **no** source or sink entries. It is irrelevant to D2/D3 and
|
|
67
|
+
relevant to D4 only as a counter-example (§8.3).
|
|
68
|
+
- `privacy-catalog.js` exports `PRIVACY_SINK_CATALOG`,
|
|
69
|
+
`PRIVACY_SINK_CATEGORIES`, and four matcher/classifier functions.
|
|
70
|
+
|
|
71
|
+
### 2.1 The classification signals are asymmetric
|
|
72
|
+
|
|
73
|
+
This is the fact that shapes everything below.
|
|
74
|
+
|
|
75
|
+
- **Sources** carry a purpose-built classification field, `provenance` — but
|
|
76
|
+
only 98 of 180 do.
|
|
77
|
+
- **Sinks** carry **none**. There is no `category` on any of the 194. The only
|
|
78
|
+
classification signal is `vuln.cwe`, which is a **vulnerability class**, not
|
|
79
|
+
a destination.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 3. The central structural finding
|
|
84
|
+
|
|
85
|
+
`schema.js`'s `SINK_CATEGORIES` is an **egress taxonomy**: it answers *where
|
|
86
|
+
does the data go* (a log, a database, an object store, an email, a model
|
|
87
|
+
provider). `catalog.js`'s sink entries are a **vulnerability taxonomy**: they
|
|
88
|
+
answer *what goes wrong* (injection, traversal, deserialization, overflow).
|
|
89
|
+
|
|
90
|
+
These are orthogonal, and they only coincide where a vulnerability happens to
|
|
91
|
+
be named after its destination. They do for SQL (→ `database`), path traversal
|
|
92
|
+
(→ `file`), SSRF (→ `external-api`), response splitting and open redirect (→
|
|
93
|
+
`http-response`). They do **not** for command injection, code injection, SSTI,
|
|
94
|
+
deserialization, XXE, XPath injection, buffer overflow, ReDoS or untrusted
|
|
95
|
+
library load — in every one of those the "destination" is **in-process
|
|
96
|
+
computation**: an interpreter, a parser, a template engine, a regex engine, a
|
|
97
|
+
dynamic loader, or raw memory. FR-201's list models none of them, correctly,
|
|
98
|
+
because none of them is egress.
|
|
99
|
+
|
|
100
|
+
**Measured: 82 of 194 sink entries (42%) fall in that bucket.** They are not
|
|
101
|
+
mapping failures. They are a category of thing FR-201 does not model, and §6.4
|
|
102
|
+
keeps every one of them in the graph rather than dropping it.
|
|
103
|
+
|
|
104
|
+
`privacy-catalog.js` is the mirror image and the reason it exists: it *is*
|
|
105
|
+
destination-shaped (log / response / outboundHttp / fileWrite / s3Upload /
|
|
106
|
+
emailSend / storage / queues / thirdPartySdk), and consequently **8 of its 9
|
|
107
|
+
categories map to `SINK_CATEGORIES` cleanly** (§5). An 18-entry inert catalog
|
|
108
|
+
therefore has better *destination* coverage than the 194-entry proven one — a
|
|
109
|
+
real, disclosable asymmetry (§7.2).
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 4. Source mapping table (complete)
|
|
114
|
+
|
|
115
|
+
Primary key: the entry's declared `provenance`.
|
|
116
|
+
|
|
117
|
+
| `provenance` | count | → `SOURCE_CATEGORIES` | `coverageStatus` | basis |
|
|
118
|
+
|---|---:|---|---|---|
|
|
119
|
+
| `http-body` | 31 | `http-body` | `modeled` | exact vocabulary match |
|
|
120
|
+
| `url-param` | 14 | `http-query` | `modeled` | pure rename |
|
|
121
|
+
| `header` | 10 | `http-header` | `modeled` | pure rename |
|
|
122
|
+
| `cookie` | 7 | `http-cookie` | `modeled` | pure rename |
|
|
123
|
+
| `path-param` | 5 | `http-route` | `modeled` | pure rename |
|
|
124
|
+
| `env` | 4 | `env-value` | `modeled` | pure rename |
|
|
125
|
+
| `cli` | 3 | `cli-argument` | `modeled` | pure rename |
|
|
126
|
+
| `network` | 8 | `external-api-response` | `modeled` ×6, **`partial` ×2** | the 6 JS/Python entries read an outbound response body (`fetch`/`axios`/`requests`/`urlopen`); the 2 `cpp` entries are refined — see §4.2 |
|
|
127
|
+
| `file-read` | 4 | `storage-read` | `modeled` ×1, **`partial` ×3** | FR-101 groups "files and object storage reads"; `storage-read` is its only encoding. The 3 `cpp` entries are refined — see §4.2 |
|
|
128
|
+
| `url-fragment` | 2 | `http-query` | **`partial`** | lossy — a fragment is URL-borne but never transmitted to the server; no `http-fragment` value exists |
|
|
129
|
+
| `stdin` | 2 | `user-input` | **`partial`** | lossy — broadens to the generic value; no stdin/console value exists |
|
|
130
|
+
| `agent-tool` | 8 | *splits* | *see below* | **the one value that does not resolve on the provenance key alone** |
|
|
131
|
+
|
|
132
|
+
### 4.1 `agent-tool` splits directionally
|
|
133
|
+
|
|
134
|
+
Its 8 entries are not one thing. Five are tool-invocation **arguments** (the
|
|
135
|
+
model chose them); three are **results/resources** flowing back. They belong in
|
|
136
|
+
different `SOURCE_CATEGORIES`, so the rule needs one documented per-entry
|
|
137
|
+
refinement:
|
|
138
|
+
|
|
139
|
+
| entry id | → category | status |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `py-mcp-tool`, `py-mcp-server-tool`, `js-mcp-call-args`, `js-mcp-request-params`, `js-mcp-extra-args` | `ai-model-output` | `partial` — the arguments *were* produced by the model, but `ai-model-output` normally denotes a completion |
|
|
142
|
+
| `js-mcp-tool-result`, `py-mcp-tool-result` | `ai-tool-result` | `modeled` |
|
|
143
|
+
| `js-mcp-resource-contents` | `ai-retrieved-document` | `modeled` |
|
|
144
|
+
|
|
145
|
+
### 4.2 C's I/O primitives are descriptor-generic — the third refinement key
|
|
146
|
+
|
|
147
|
+
The `network` and `file-read` rows above cannot claim `modeled` for their C
|
|
148
|
+
entries, under §6.1's own bar ("resolves 1:1 with no semantic loss"). C's I/O
|
|
149
|
+
primitives are **descriptor-generic**: the same call reads a file, a socket, a
|
|
150
|
+
pipe or stdin, and the catalog entry has no way to say which.
|
|
151
|
+
|
|
152
|
+
| entry | provenance | why not `modeled` |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `cpp-recv`, `cpp-recvfrom` | `network` | directionally ambiguous — on a *client* socket this is an API response; on a *listening* socket it is an inbound client request, which `external-api-response` actively mis-describes |
|
|
155
|
+
| `cpp-read` | `file-read` | a raw fd is equally a file, socket or pipe |
|
|
156
|
+
| `cpp-fread`, `cpp-fgets` | `file-read` | a `FILE*` is commonly `stdin`, not a file |
|
|
157
|
+
|
|
158
|
+
The refinement key is therefore **`language === 'cpp'`** within these two
|
|
159
|
+
provenance buckets: category unchanged, status demoted to `partial`, caveat
|
|
160
|
+
stated. This is the third and last documented refinement, alongside §4.1's
|
|
161
|
+
per-entry-id one and §5.2's `framework` one — and it is the same *shape* as
|
|
162
|
+
both: a declared classification value that is correct for most of its entries
|
|
163
|
+
and lossy for an identifiable subset. (§4.3's 82-row override table is a
|
|
164
|
+
different mechanism entirely — a fallback for entries with *no* declared value,
|
|
165
|
+
not a refinement of one.) Note the `stdin` provenance (§4, also `cpp`-only) was
|
|
166
|
+
already `partial` for the adjacent reason, so the C entries are now uniformly
|
|
167
|
+
honest across all three buckets.
|
|
168
|
+
|
|
169
|
+
### 4.3 The 82 entries with no `provenance`
|
|
170
|
+
|
|
171
|
+
They get a per-entry-id override table (enumerated in full in the PoC file's
|
|
172
|
+
`NO_PROVENANCE_OVERRIDES`), and every one of them is **`candidate`** — see
|
|
173
|
+
§6.3 for why. The gap is not random: **`go`, `java`, `rb` and `php` declare
|
|
174
|
+
`provenance` on literally zero of their source entries**, so entire language
|
|
175
|
+
families would otherwise be uncategorized.
|
|
176
|
+
|
|
177
|
+
Resulting source coverage: **84 `modeled`, 14 `partial`, 82 `candidate`, 0
|
|
178
|
+
`unsupported`.**
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 5. Sink mapping tables (complete)
|
|
183
|
+
|
|
184
|
+
### 5.1 `catalog.js` — primary key `vuln.cwe`
|
|
185
|
+
|
|
186
|
+
| CWE | count | → `SINK_CATEGORIES` | status | basis |
|
|
187
|
+
|---|---:|---|---|---|
|
|
188
|
+
| CWE-89 | 38 | `database` | `modeled` | a SQL query call is unambiguously a database destination |
|
|
189
|
+
| CWE-943 | 1 | `database` | `modeled` | NoSQL `$where` |
|
|
190
|
+
| CWE-22 | 16 | `file` | `modeled` | traversal sinks are filesystem reads/writes |
|
|
191
|
+
| CWE-73 | 1 | `file` | `modeled` | arbitrary file write |
|
|
192
|
+
| CWE-918 | 15 | `external-api` | `modeled` | SSRF sinks are outbound HTTP client calls |
|
|
193
|
+
| CWE-601 | 7 | `http-response` | `modeled` | a redirect is a response header |
|
|
194
|
+
| CWE-113 | 9 | `http-response` | `modeled` | the sink *is* the response-header writer |
|
|
195
|
+
| CWE-79 | 16 | *splits* | *see 5.2* | |
|
|
196
|
+
| CWE-90 | 9 | `database` | **`candidate`** | an LDAP directory is a queryable store, structurally like a DB — but FR-201 never names directory services |
|
|
197
|
+
| CWE-78 | 29 | — | **`unsupported`** | shell/process execution |
|
|
198
|
+
| CWE-95 | 5 | — | **`unsupported`** | code evaluation — destination is an interpreter |
|
|
199
|
+
| CWE-94 | 10 | — | **`unsupported`** | code injection / template compilation |
|
|
200
|
+
| CWE-1336 | 2 | — | **`unsupported`** | SSTI — destination is a template engine |
|
|
201
|
+
| CWE-502 | 10 | — | **`unsupported`** | destination is a deserializer |
|
|
202
|
+
| CWE-611 | 10 | — | **`unsupported`** | XXE — destination is an XML parser |
|
|
203
|
+
| CWE-643 | 10 | — | **`unsupported`** | XPath — destination is an in-memory query engine |
|
|
204
|
+
| CWE-120 | 3 | — | **`unsupported`** | destination is raw memory |
|
|
205
|
+
| CWE-787 | 1 | — | **`unsupported`** | destination is raw memory |
|
|
206
|
+
| CWE-1333 | 1 | — | **`unsupported`** | destination is the regex engine |
|
|
207
|
+
| CWE-114 | 1 | — | **`unsupported`** | destination is the dynamic loader |
|
|
208
|
+
|
|
209
|
+
CWE-94 and CWE-1336 are both `unsupported` **deliberately and uniformly**,
|
|
210
|
+
even though a rendered template's output usually does reach a response: the
|
|
211
|
+
catalog entry names the *compile* call, not the response write, so a
|
|
212
|
+
`http-response` mapping would be an inference about a different program point.
|
|
213
|
+
Treating SSTI as egress while treating `eval` as non-egress would be special
|
|
214
|
+
pleading; the rule is "the entry's own callee decides", applied consistently.
|
|
215
|
+
|
|
216
|
+
### 5.2 CWE-79's refinement — the sink side's one exception
|
|
217
|
+
|
|
218
|
+
CWE-79 covers two genuinely different destinations, split on `framework`:
|
|
219
|
+
|
|
220
|
+
| `framework` | entries | → category | status |
|
|
221
|
+
|---|---:|---|---|
|
|
222
|
+
| `dom`, `react` | 6 | `client-storage` | **`partial`** |
|
|
223
|
+
| everything else (`express`, `koa`, `servlet`, `flask`, `rails`, `ktor`, `aspnet`, `fmt`, `core`) | 10 | `http-response` | `modeled` |
|
|
224
|
+
|
|
225
|
+
The `partial` is a **naming gap in `schema.js`, disclosed not patched** (§7.4):
|
|
226
|
+
FR-201's bullet reads "browser DOM **or** client storage", and `schema.js`
|
|
227
|
+
encodes that whole bullet as the single value `client-storage`. So
|
|
228
|
+
`client-storage` is the *correct* target per the PRD's own text while
|
|
229
|
+
under-naming the DOM half. `innerHTML` is not client storage in any ordinary
|
|
230
|
+
reading of the phrase, and a reader of the graph will be misled unless the
|
|
231
|
+
status says `partial`.
|
|
232
|
+
|
|
233
|
+
Resulting sink coverage: **97 `modeled`, 6 `partial`, 9 `candidate`, 82
|
|
234
|
+
`unsupported`.**
|
|
235
|
+
|
|
236
|
+
### 5.3 `privacy-catalog.js` ↔ `SINK_CATEGORIES`, entry by entry
|
|
237
|
+
|
|
238
|
+
| privacy `category` | entries | → `SINK_CATEGORIES` | status | basis |
|
|
239
|
+
|---|---:|---|---|---|
|
|
240
|
+
| `log` | 4 | `log` | `modeled` | `console.log/error`, `logger.info/warn` |
|
|
241
|
+
| `response` | 2 | `http-response` | `modeled` | `res.send` / `res.json` |
|
|
242
|
+
| `outboundHttp` | 2 | `external-api` | `modeled` | `fetch` / `axios.post` |
|
|
243
|
+
| `fileWrite` | 2 | `file` | `modeled` | `fs.writeFile` / `writeFileSync` |
|
|
244
|
+
| `s3Upload` | 1 | `object-storage` | `modeled` | `s3.putObject` |
|
|
245
|
+
| `emailSend` | 1 | `email` | `modeled` | nodemailer `sendMail` |
|
|
246
|
+
| `storage` | 2 | `database` | `modeled` | **does not split** — see below |
|
|
247
|
+
| `queues` | 2 | `queue` | `modeled` | `sqs.sendMessage` / `sns\|kafka publish`; FR-201's `queue` covers "queues, topics, streams, and event buses" |
|
|
248
|
+
| `thirdPartySdk` | 2 | `analytics` | **`partial`** | **does split** — see below |
|
|
249
|
+
|
|
250
|
+
**`storage` does NOT split**, contrary to the scoping doc's expectation that it
|
|
251
|
+
would need splitting across `database`/`object-storage`/`cache`. Both entries
|
|
252
|
+
carrying it are mongodb `insertOne`/`insertMany` with a `collection|db|mongo`
|
|
253
|
+
receiver. The *name* is broader than its entries; the entries are not. D1/4b
|
|
254
|
+
pins this so a future redis or S3 entry filed under `storage` fails loudly
|
|
255
|
+
instead of being silently mapped to `database`.
|
|
256
|
+
|
|
257
|
+
**`thirdPartySdk` DOES split, and cannot be resolved by a registry at all.**
|
|
258
|
+
Its two entries share one `receiverTypeIn` alternation:
|
|
259
|
+
`stripe|sentry|datadog|segment|amplitude|mixpanel|posthog|braze|intercom|analytics`.
|
|
260
|
+
That single regex spans four different `SINK_CATEGORIES` — `analytics`
|
|
261
|
+
(segment/amplitude/mixpanel/posthog), `monitoring` (sentry/datadog),
|
|
262
|
+
`external-api` (stripe) and `collaboration` (intercom/braze). Which one is
|
|
263
|
+
correct is only decidable **at match time**, from the receiver that actually
|
|
264
|
+
matched — information a registry reclassifying *entries* never sees. It
|
|
265
|
+
therefore resolves to `analytics` (the plurality) with status `partial`, and
|
|
266
|
+
§9's D3 checklist carries the match-time refinement forward.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 6. The `coverageStatus` decision procedure
|
|
271
|
+
|
|
272
|
+
Given a catalog entry, apply in order. The procedure is **total**: every one of
|
|
273
|
+
the 756 entries gets a decision, and D1/1e proves it.
|
|
274
|
+
|
|
275
|
+
### 6.1 `modeled`
|
|
276
|
+
|
|
277
|
+
The entry carries an **explicitly declared classification field** — a source's
|
|
278
|
+
`provenance`, a privacy sink's `category` — or, for a `catalog.js` sink, a
|
|
279
|
+
`vuln.cwe` whose vulnerability class *is* its destination; **and** that value
|
|
280
|
+
resolves to exactly one `SOURCE_CATEGORIES`/`SINK_CATEGORIES` value with no
|
|
281
|
+
semantic loss.
|
|
282
|
+
|
|
283
|
+
### 6.2 `partial`
|
|
284
|
+
|
|
285
|
+
Same evidentiary basis as `modeled` — the classification is author-declared —
|
|
286
|
+
but the resolution is **lossy in a stated way**. Two loss shapes occur, and
|
|
287
|
+
both must be named in the entry's `reason`:
|
|
288
|
+
|
|
289
|
+
- **Broadening**: the target value is strictly more general than the source
|
|
290
|
+
fact (`stdin` → `user-input`).
|
|
291
|
+
- **Narrowing / adjacency**: the target value is the closest available but is
|
|
292
|
+
not the same thing (`url-fragment` → `http-query`; DOM XSS →
|
|
293
|
+
`client-storage`; `thirdPartySdk` → `analytics`).
|
|
294
|
+
|
|
295
|
+
`partial` is *not* "we are unsure". It is "we are sure, and the vocabulary
|
|
296
|
+
cannot say it exactly." That distinction is the whole reason it is separate
|
|
297
|
+
from `candidate`.
|
|
298
|
+
|
|
299
|
+
### 6.3 `candidate`
|
|
300
|
+
|
|
301
|
+
The entry carries **no declared classification field**, and the category is
|
|
302
|
+
inferred from secondary metadata the author wrote for a different purpose —
|
|
303
|
+
the entry's `id`, `label`, or `framework`. The mapping is plausible and
|
|
304
|
+
reviewed, but nobody asserted it.
|
|
305
|
+
|
|
306
|
+
This tier has a second, operational meaning that matters more than the first:
|
|
307
|
+
**a `candidate` mapping does not generalize.** It comes from a per-entry
|
|
308
|
+
override table, so a *newly added* catalog entry of the same shape gets no
|
|
309
|
+
mapping at all and must fail the registry's own completeness check rather than
|
|
310
|
+
silently inheriting one. `modeled`/`partial` mappings are keyed on a declared
|
|
311
|
+
value and do generalize.
|
|
312
|
+
|
|
313
|
+
All 82 unprovenanced sources are `candidate`. Not because
|
|
314
|
+
`py-flask-request-cookies → http-cookie` is doubtful — it plainly is not — but
|
|
315
|
+
because the evidence for it is a human reading a label, and the graph should
|
|
316
|
+
say so.
|
|
317
|
+
|
|
318
|
+
### 6.4 `unsupported`
|
|
319
|
+
|
|
320
|
+
**No `SOURCE_CATEGORIES`/`SINK_CATEGORIES` value models the entry's real
|
|
321
|
+
semantics.** Category is `null`.
|
|
322
|
+
|
|
323
|
+
Critically, `unsupported` is **not** exclusion. AC-11 requires a detected
|
|
324
|
+
source or sink with no resolved path to be "visible with a coverage reason",
|
|
325
|
+
and FR-201 requires all supported sinks to "remain discoverable". So an
|
|
326
|
+
`unsupported` entry still becomes a node: it keeps a `NODE_KINDS` value
|
|
327
|
+
(§7.1's `process`), keeps its label and location, and carries a non-empty
|
|
328
|
+
`reason` string. D1/1e asserts the non-empty reason on every entry precisely
|
|
329
|
+
so this cannot regress into a silent drop — the §18.4 failure mode this whole
|
|
330
|
+
PRD treats as load-bearing.
|
|
331
|
+
|
|
332
|
+
### 6.5 `manual` — the open question, resolved
|
|
333
|
+
|
|
334
|
+
**`manual` means the classification was asserted by a human, not derived by
|
|
335
|
+
the analyzer. No registry can ever emit it, and D2/D3 must not.**
|
|
336
|
+
|
|
337
|
+
Three independent lines of evidence, all checked in this increment:
|
|
338
|
+
|
|
339
|
+
1. **Codebase precedent, consistent across four modules.**
|
|
340
|
+
`posture/privacy-framework.js` defines `BUCKETS = ['gap', 'engine-gap',
|
|
341
|
+
'manual', 'satisfied']` and selects between the middle two on
|
|
342
|
+
`codeTestable === 'no' ? 'manual' : 'engine-gap'` — *manual* is
|
|
343
|
+
"not code-testable at all", explicitly distinguished from *engine-gap*,
|
|
344
|
+
"code-testable but unimplemented". `posture/auditor-walkthrough.js`
|
|
345
|
+
documents `'manual'` as "requires manual attestation".
|
|
346
|
+
`lineage/protection.js`'s `EVIDENCE_GRADES` and `schema.js`'s
|
|
347
|
+
`EVIDENCE_TYPES` both carry `'manual'` in the same human-asserted sense.
|
|
348
|
+
That `engine-gap` / `manual` pair is the exact analogue of the
|
|
349
|
+
`unsupported` / `manual` pair here.
|
|
350
|
+
|
|
351
|
+
2. **The PRD's own text.** FR-101's acceptance clause reads: "**All Sources**
|
|
352
|
+
shows both connected and disconnected sources, with `modeled`, `partial`,
|
|
353
|
+
`candidate`, or `unsupported` status." Four values. `manual` is deliberately
|
|
354
|
+
absent from the *inventory* clause while present in the §10.3 *node
|
|
355
|
+
contract* — i.e. a node may be `manual`; a discovered-source inventory row
|
|
356
|
+
may not.
|
|
357
|
+
|
|
358
|
+
3. **Construction.** A catalog entry is analyzer-derived by definition. There
|
|
359
|
+
is no path by which reading one produces a human assertion.
|
|
360
|
+
|
|
361
|
+
`manual` is therefore reserved for a later increment in which an operator
|
|
362
|
+
**declares** a node — pairing with the `declared` value that already exists in
|
|
363
|
+
both `SOURCE_CATEGORIES` and `SINK_CATEGORIES`, and with
|
|
364
|
+
`evidenceType: 'manual'`. That is also exactly why `declared` shows up in §7.2
|
|
365
|
+
as unreachable: it is the operator-declaration path, and nothing implements it
|
|
366
|
+
yet.
|
|
367
|
+
|
|
368
|
+
D1/2a asserts no entry in any of the three catalogs yields `manual`. D1/2b
|
|
369
|
+
asserts the schema's five values minus the four a registry may emit is exactly
|
|
370
|
+
`['manual']` — so if a sixth status is ever added, this file's premise fails
|
|
371
|
+
loudly rather than quietly.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## 7. Node kind, externality, and the disclosed gaps
|
|
376
|
+
|
|
377
|
+
### 7.1 Node-kind assignment rule
|
|
378
|
+
|
|
379
|
+
**Sources: always `NODE_KINDS.source`.** Checked, not assumed (D1/3b). The
|
|
380
|
+
alternative considered was `boundary`; it is wrong, because `boundary` models a
|
|
381
|
+
**trust-zone crossing**, which is a property of an edge between two systems,
|
|
382
|
+
and no catalog entry carries system, zone, or deployment information at all
|
|
383
|
+
(D1/8d).
|
|
384
|
+
|
|
385
|
+
**Sinks: derived from the resolved category, never from the entry.** The
|
|
386
|
+
category → kind table is in the PoC's `CATEGORY_NODE_KIND`; its shape is
|
|
387
|
+
`log`/`stdout` → `log`; every store-ish category (`database`, `file`,
|
|
388
|
+
`object-storage`, `cache`, `client-storage`, `backup`, `export`) → `store`;
|
|
389
|
+
`queue` → `queue`; every remote-party category (`external-api`, `webhook`,
|
|
390
|
+
`email`, `sms`, `push-notification`, `collaboration`, `analytics`,
|
|
391
|
+
`monitoring`, all `ai-*` except `ai-local-model`) → `external`;
|
|
392
|
+
`http-response` and `declared` → `sink`.
|
|
393
|
+
|
|
394
|
+
**The four `NODE_KINDS` values a reclassified sink never gets, and why** —
|
|
395
|
+
recorded so a later reader sees a decision rather than an oversight.
|
|
396
|
+
`api` was considered for `external-api`/`webhook` and rejected: it denotes a
|
|
397
|
+
*service endpoint entity*, whereas a catalog sink is the **call site** that
|
|
398
|
+
sends data to one, and `external` is the value that says "the data leaves to
|
|
399
|
+
a party outside this program". (When Sub-project E builds real destination
|
|
400
|
+
nodes from FR-202 resolution, *those* may well be `api`; the registry's
|
|
401
|
+
call-site node is not.) `transform` belongs to `TRANSFORM_KINDS`-shaped
|
|
402
|
+
transformation entities (§8), not to sinks. `boundary` is rejected for the
|
|
403
|
+
same reason it is on the source side — it needs trust-zone data no entry
|
|
404
|
+
carries. `unresolved` is rejected **here** but is genuinely needed one layer
|
|
405
|
+
out, for a *dynamic destination* rather than an unmappable category — see
|
|
406
|
+
§9's D3 checklist item 5 and the degraded-dead-end rule in §9's
|
|
407
|
+
carried-forward section.
|
|
408
|
+
|
|
409
|
+
**A `null` category (i.e. `unsupported`) → `process`.** This is not a fallback;
|
|
410
|
+
it is the *explanation*. Every unsupported sink is an in-process computation
|
|
411
|
+
destination, which is precisely why FR-201's egress taxonomy has no value for
|
|
412
|
+
it. D1/3c pins the biconditional: `kind === 'process'` **iff**
|
|
413
|
+
`coverageStatus === 'unsupported'`. Measured sink kinds across all 194:
|
|
414
|
+
exactly `{external, process, sink, store}`.
|
|
415
|
+
|
|
416
|
+
> **The biconditional is contingent, and whoever breaks it should know they
|
|
417
|
+
> are meant to.** It holds today only because the one *other* category that
|
|
418
|
+
> maps to `process` — `ai-local-model` — is **vacuously unreachable** (§7.2:
|
|
419
|
+
> no catalog entry produces any `ai-*` sink). The moment AI-destination
|
|
420
|
+
> detection lands — which §7.2 names as the single largest piece of future
|
|
421
|
+
> work here — an `ai-local-model` sink will have `kind: 'process'` with a
|
|
422
|
+
> non-null category and a non-`unsupported` status, and `D1/3c`'s reverse
|
|
423
|
+
> implication (`kind === 'process'` ⟹ `unsupported`) becomes false.
|
|
424
|
+
> **That is a correct consequence of new coverage, not a regression.**
|
|
425
|
+
> Whoever adds AI-sink detection owns relaxing the assertion to its forward
|
|
426
|
+
> direction only (`unsupported` ⟹ `process`), which is the half that
|
|
427
|
+
> actually encodes §3's claim. The assertion is written as a biconditional
|
|
428
|
+
> deliberately, because until then the stronger form is true and catches
|
|
429
|
+
> more; it is flagged here so it is not mistaken for something the AI work
|
|
430
|
+
> broke.
|
|
431
|
+
|
|
432
|
+
### 7.2 Categories no catalog can reach today
|
|
433
|
+
|
|
434
|
+
Measured, and pinned as exact lists so they cannot go stale silently.
|
|
435
|
+
|
|
436
|
+
**`SOURCE_CATEGORIES`: 14 of 21 reachable.** Unreachable:
|
|
437
|
+
`graphql-argument`, `grpc-field`, `queue-message`, `database-read`,
|
|
438
|
+
`webhook-payload`, `ai-memory`, `declared`.
|
|
439
|
+
|
|
440
|
+
All seven are expected. GraphQL and gRPC have no source entries in `catalog.js`
|
|
441
|
+
at all; queue-message and database-read are ingress shapes the scanner does not
|
|
442
|
+
pattern-match; `declared` is the operator-declaration path (§6.5).
|
|
443
|
+
|
|
444
|
+
**`SINK_CATEGORIES`: only 10 of 29 reachable, as measured at D1's time of
|
|
445
|
+
writing** — even counting `privacy-catalog.js`. Unreachable at THAT time:
|
|
446
|
+
`stdout`, `cache`, `monitoring`, `sms`, `push-notification`, `collaboration`,
|
|
447
|
+
`webhook`, `backup`, `export`, `declared`, and **all nine `ai-*` values**
|
|
448
|
+
(`ai-model-provider`, `ai-local-model`, `ai-agent`, `ai-tool`,
|
|
449
|
+
`ai-vector-store`, `ai-memory`, `ai-training`, `ai-evaluation`,
|
|
450
|
+
`ai-telemetry`).
|
|
451
|
+
|
|
452
|
+
**CORRECTED (Sub-project H, AC-07 closure, 2026-08-31): `ai-model-provider`
|
|
453
|
+
is no longer unreachable.** `dataflow/catalog.js` gained four real sink
|
|
454
|
+
entries (OpenAI `chat.completions.create`/`responses.create`, Anthropic
|
|
455
|
+
`messages.create`, AWS Bedrock `InvokeModelCommand`) and `sink-registry.js`
|
|
456
|
+
gained a `CWE-201` → `ai-model-provider` `CWE_MAP` row — see
|
|
457
|
+
`docs/superpowers/plans/2026-08-31-data-flow-explorer-m1-subproject-h-ac07.md`.
|
|
458
|
+
**11 of 29 `SINK_CATEGORIES` are now reachable**, not 10; the other eight
|
|
459
|
+
`ai-*` values remain unreachable (`ai-local-model`/`ai-agent`/`ai-tool`/
|
|
460
|
+
`ai-vector-store`/`ai-memory`/`ai-training`/`ai-evaluation`/`ai-telemetry`),
|
|
461
|
+
and every OTHER category named unreachable above is still unreachable —
|
|
462
|
+
this correction is scoped to `ai-model-provider` alone. Every downstream
|
|
463
|
+
reference to "10 of 29" / "19 unreachable" / "all nine `ai-*`" elsewhere in
|
|
464
|
+
this document (§9.2's exit criterion discussion, D5's own historical
|
|
465
|
+
measurement) is a HISTORICAL record of what was true when D1-D5 shipped,
|
|
466
|
+
not a live claim — read it as "at the time," not as the current state.
|
|
467
|
+
|
|
468
|
+
**The headline gap this section originally recorded: FR-205 (AI
|
|
469
|
+
destinations) had zero sink-catalog coverage.** The source side *did* have
|
|
470
|
+
AI coverage even then — the eight MCP entries reach three `ai-*` source
|
|
471
|
+
categories — so the asymmetry was real and specific: this scanner could see
|
|
472
|
+
data arriving *from* an agent context and could not see data being sent *to*
|
|
473
|
+
a model provider, vector store, or agent. AC-07 ("PHI enters an
|
|
474
|
+
Anthropic/OpenAI/Bedrock model request") was, AT THAT TIME, **not
|
|
475
|
+
satisfiable** by reclassification alone — that gap is what the correction
|
|
476
|
+
above closes for the `ai-model-provider` slice of it (a direct API call to
|
|
477
|
+
a hosted model provider). `ai-agent`/`ai-tool`/the other seven `ai-*`
|
|
478
|
+
categories remain genuinely unreachable and are NOT closed by this
|
|
479
|
+
correction — they need their own, separate catalog work, deliberately out
|
|
480
|
+
of the AC-07 closure's scope.
|
|
481
|
+
|
|
482
|
+
**Whose problem this is: Sub-project H, inside Milestone 1 — not Milestone 2.**
|
|
483
|
+
AC-07 sits in **Milestone 1's own exit gate** (PRD §26, quoted verbatim in the
|
|
484
|
+
parent M1 scoping doc: *"AC-01, AC-02, AC-07, AC-11, and schema completeness
|
|
485
|
+
pass on the supported-language corpus"*), and that doc's §5 table assigns
|
|
486
|
+
running it to **Sub-project H** (*"Runs AC-01, AC-02, AC-07, AC-11, and schema
|
|
487
|
+
completeness against the real JS/TS corpus"*). So this is not a
|
|
488
|
+
deferred-to-a-later-milestone concern that D may hand off and forget: it is a
|
|
489
|
+
gap in the milestone D itself belongs to, and H will hit it as a hard exit-gate
|
|
490
|
+
failure unless AI-destination detection lands somewhere in D through G first.
|
|
491
|
+
**Whoever scopes Sub-project H must budget for that**, and should treat "AC-07
|
|
492
|
+
passes" as blocked on new detection work that no current increment owns.
|
|
493
|
+
|
|
494
|
+
`stdout` is likewise unreachable despite FR-201 naming it, because
|
|
495
|
+
`console.log` is catalogued as `log`, not split.
|
|
496
|
+
|
|
497
|
+
### 7.3 Five places the upstream documents were measurably wrong
|
|
498
|
+
|
|
499
|
+
Recorded because later increments will otherwise re-inherit them. "Upstream"
|
|
500
|
+
rather than "the scoping doc" deliberately — items 3 and 5 originate in the D1
|
|
501
|
+
task brief, not the scoping doc, and attributing everything to one source would
|
|
502
|
+
send a corrector to the wrong file.
|
|
503
|
+
|
|
504
|
+
1. **Catalog size.** The scoping doc (and `scanner/src/dataflow/CLAUDE.md`)
|
|
505
|
+
say **655 entries (149 source / 124 sink / 382 sanitizer)**. The live
|
|
506
|
+
figures are **756 (180 / 194 / 382)**. `dataflow/CLAUDE.md` itself warns
|
|
507
|
+
this number "has already drifted once"; it has now drifted twice.
|
|
508
|
+
Pinned by D1/8a.
|
|
509
|
+
2. **The privacy vocabulary.** The scoping doc names eight categories —
|
|
510
|
+
`log`/`response`/`storage`/`queues`/`email`/`file`/`outbound`/`third-party`.
|
|
511
|
+
The real vocabulary is **nine**, and four of those eight names do not exist:
|
|
512
|
+
the real identifiers are `emailSend`, `fileWrite`, `outboundHttp`,
|
|
513
|
+
`thirdPartySdk`, plus `s3Upload`, which the eight-value list omits entirely.
|
|
514
|
+
Pinned by D1/4d.
|
|
515
|
+
3. **`storage` splitting.** The **task brief** (not the scoping doc, which only
|
|
516
|
+
says the two vocabularies don't match) predicted `storage` would need
|
|
517
|
+
splitting across `database`/`object-storage`/`cache`. Measured: it does not
|
|
518
|
+
(§5.3). Pinned by D1/4b.
|
|
519
|
+
4. **The sink classification field.** The scoping doc calls it `vuln`. That is
|
|
520
|
+
correct as far as it goes, but the operative sub-field is `vuln.cwe`, and
|
|
521
|
+
the more important fact — that **no sink entry carries a `category` field at
|
|
522
|
+
all** — is not recorded there. Pinned by D1/8b.
|
|
523
|
+
5. **`SINK_CATEGORIES` has 29 values, not 28.** Both the scoping doc (D3's row:
|
|
524
|
+
*"more categories: 28 vs 21"*) and the task brief say 28. The live export
|
|
525
|
+
has **29** (`SOURCE_CATEGORIES`' 21 is correct). This document uses 29
|
|
526
|
+
throughout — §7.2's "10 of 29 reachable" and "19 unreachable" sum correctly
|
|
527
|
+
— but the correction was not flagged alongside the others in D1's first
|
|
528
|
+
draft, so it is recorded here. It matters for D3's sizing, and any
|
|
529
|
+
coverage-fraction computed off 28 is wrong. Pinned by D1/6c.
|
|
530
|
+
|
|
531
|
+
### 7.4 Two genuine schema/catalog gaps — named, deliberately not patched
|
|
532
|
+
|
|
533
|
+
Per this plan's Global Constraints, both are reported rather than fixed.
|
|
534
|
+
|
|
535
|
+
**(a) `catalog.js` is missing `provenance` on 82 source entries.** This is a
|
|
536
|
+
*catalog* defect, not a lineage-side one, and it is the direct cause of 46% of
|
|
537
|
+
sources being `candidate` rather than `modeled`. It looks trivially fixable and
|
|
538
|
+
is not: `provenance` is **not inert**. `dataflow/engine.js` propagates it onto
|
|
539
|
+
findings as `sourceProvenance` and `chain[].provenance`, and
|
|
540
|
+
`src/report/index.js` emits it. Adding it to 82 entries would change reported
|
|
541
|
+
finding content for go/java/rb/php/py flows across the corpus. That is a real,
|
|
542
|
+
benchmarked change and must be its own increment with `bench:cve-replay:check`
|
|
543
|
+
and `bench:layer-recall:check` run against it — **not** a side effect of a
|
|
544
|
+
lineage design task. Until then, D2 carries the override table and honestly
|
|
545
|
+
reports `candidate`.
|
|
546
|
+
|
|
547
|
+
**(b) `schema.js`'s `client-storage` under-names FR-201's "browser DOM or
|
|
548
|
+
client storage".** Six DOM XSS sinks (`document.write`, `innerHTML`,
|
|
549
|
+
`outerHTML`, `insertAdjacentHTML`, both `dangerouslySetInnerHTML` forms) have
|
|
550
|
+
the rendered DOM as their destination. `client-storage` is the PRD-faithful
|
|
551
|
+
target and a misleading label for them. A future additive `browser-dom` value
|
|
552
|
+
would promote all six from `partial` to `modeled`, but adding a
|
|
553
|
+
`SINK_CATEGORIES` value is a **schema-version-bumping change** (`schema.js`'s
|
|
554
|
+
own header says so) requiring `dataflow-graph.schema.json` and
|
|
555
|
+
`validate.js` to move in lockstep. Out of scope for a design increment; raised
|
|
556
|
+
here for whoever owns the next schema bump.
|
|
557
|
+
|
|
558
|
+
Neither `catalog.js`, `catalog-expanded.js`, `privacy-catalog.js` nor
|
|
559
|
+
`schema.js` was modified by this increment.
|
|
560
|
+
|
|
561
|
+
### 7.5 Externality: derivable, but only weakly
|
|
562
|
+
|
|
563
|
+
`schema.js`'s node contract requires `externality` ∈
|
|
564
|
+
`{internal, external, unknown}` "plus evidence" (PRD §10.3).
|
|
565
|
+
|
|
566
|
+
**No catalog entry carries any destination information** — no host, URL,
|
|
567
|
+
provider, system, or externality field. D1/8d asserts this across all 756
|
|
568
|
+
entries. Externality therefore **cannot** be entry-derived and must be
|
|
569
|
+
**category-derived**, per the `CATEGORY_EXTERNALITY` table: `log`, `stdout`,
|
|
570
|
+
`http-response`, `file`, `client-storage`, `ai-local-model` and every
|
|
571
|
+
`unsupported`/`process` sink → `internal`; every remote-party category →
|
|
572
|
+
`external`; every store-or-queue category (`database`, `object-storage`,
|
|
573
|
+
`cache`, `queue`, `backup`, `export`, `declared`) → **`unknown`**, because a
|
|
574
|
+
database may equally be a local container or a managed cloud service and the
|
|
575
|
+
entry gives no way to tell.
|
|
576
|
+
|
|
577
|
+
The consequence D2/D3 must respect: the accompanying evidence grade is at best
|
|
578
|
+
`declared` (it comes from a registry table), **never** `code`. Real externality
|
|
579
|
+
resolution is FR-202's job and lands in Milestone 2. Emitting `external` with a
|
|
580
|
+
`code` grade off this table would be an unsupported claim.
|
|
581
|
+
|
|
582
|
+
**2026-08-31 implementation note (D2, Task 1):** §9.0's decision shape —
|
|
583
|
+
`{kind, category, coverageStatus, externality, reason}` — requires
|
|
584
|
+
`externality` on BOTH registries' output, but the `CATEGORY_EXTERNALITY`
|
|
585
|
+
table above only ever covers `SINK_CATEGORIES`; no source-side twin
|
|
586
|
+
existed anywhere in this document. This went undetected through D1's own
|
|
587
|
+
review, fix round, and scoped re-review because the design-task PoC's own
|
|
588
|
+
totality test (`D1/1e`) asserted the `externality` enum only inside its
|
|
589
|
+
SINKS loop, never its SOURCES loop — the field was simply never exercised
|
|
590
|
+
for a source, so the gap was invisible to every check that ran.
|
|
591
|
+
|
|
592
|
+
D2 closed it by building **`SOURCE_CATEGORY_EXTERNALITY`**, in
|
|
593
|
+
`source-registry.js`, deriving all 21 `SOURCE_CATEGORIES` values via the
|
|
594
|
+
SAME rule this section's own reasoning already states for sinks — read
|
|
595
|
+
symmetrically: *is the counterparty the data came from a party outside
|
|
596
|
+
this program?* An end user hitting your own HTTP/GraphQL/gRPC/CLI
|
|
597
|
+
endpoint, or your own process's env/argv, is your front door → `internal`
|
|
598
|
+
(the flagship fixture's own `source`/`web-app` node pins exactly this:
|
|
599
|
+
`externality: 'internal'`, `fixtures/build-flagship-fixture.mjs:119`).
|
|
600
|
+
Another service's response, an AI provider's output, or an inbound
|
|
601
|
+
webhook payload is a genuine outside party → `external`. A store/queue
|
|
602
|
+
read where the entry gives no way to tell what's on the other end →
|
|
603
|
+
`unknown` (mirroring the sink table's own reasoning for `database`/
|
|
604
|
+
`object-storage`/etc.).
|
|
605
|
+
|
|
606
|
+
Two deliberate asymmetries with the sink table, so a reader doesn't mistake
|
|
607
|
+
either for drift: (1) the null-category (`unsupported`) fallback is
|
|
608
|
+
`externality: 'unknown'` on the source side, not `internal` as the sink
|
|
609
|
+
table gives it — a sink's `unsupported` still means "the call site is in
|
|
610
|
+
YOUR process," which is genuinely `internal`; a source's `unsupported`
|
|
611
|
+
means "we don't know what this construct even is," which has no honest
|
|
612
|
+
`internal` claim to make. (2) `storage-read` (D1's own merge of what would,
|
|
613
|
+
on the sink side, be two distinct categories — `file` and
|
|
614
|
+
`object-storage`, `internal` and `unknown` respectively) resolves to
|
|
615
|
+
`unknown`, the more conservative of the two, since the merged category
|
|
616
|
+
genuinely cannot distinguish which case a given entry is — never
|
|
617
|
+
`internal`-by-default.
|
|
618
|
+
|
|
619
|
+
The evidence-grade consequence above (`declared`, never `code`) applies
|
|
620
|
+
identically to `SOURCE_CATEGORY_EXTERNALITY` — nothing about deriving
|
|
621
|
+
externality from a table rather than an entry changes which grade that
|
|
622
|
+
derivation earns.
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## 8. Confirmations for D4 (transformation-kind recognition)
|
|
627
|
+
|
|
628
|
+
D4 is not designed here. Three facts it depends on were checked so it starts
|
|
629
|
+
from a settled base.
|
|
630
|
+
|
|
631
|
+
**8.1 `TRANSFORM_KINDS` is correct and complete.** It matches PRD §10.6's
|
|
632
|
+
prose exactly, in order, 13 values, no duplicates:
|
|
633
|
+
`mask, redact, tokenize, hash, encrypt, decrypt, encode, decode, aggregate,
|
|
634
|
+
truncate, normalize, custom, unknown`. `REVERSIBILITY_VALUES` likewise matches
|
|
635
|
+
"reversible, irreversible, unknown". **No change is needed** — D4 can treat the
|
|
636
|
+
vocabulary as fixed. Pinned by D1/7a, which also asserts `mask`/`hash`/
|
|
637
|
+
`tokenize`/`encrypt` remain four distinct values, per §10.6's explicit "must
|
|
638
|
+
never be treated as synonyms".
|
|
639
|
+
|
|
640
|
+
**8.2 The 706 sanitizer entries are the WRONG input for D4.** `catalog.js`'s
|
|
641
|
+
382 sanitizers and `catalog-expanded.js`'s 324 are keyed on `effect`
|
|
642
|
+
(`strip`/`taintNever`/`taintIf-not-pinned`) and `appliesTo` (a *threat class*,
|
|
643
|
+
e.g. XSS vs SQLi). They answer "does this neutralize this vulnerability
|
|
644
|
+
family", not "what kind of transformation is this and is it reversible".
|
|
645
|
+
Reclassifying them into `TRANSFORM_KINDS` would be a category error — an HTML
|
|
646
|
+
escaper is `encode`, but 380-odd others carry no signal that maps at all.
|
|
647
|
+
|
|
648
|
+
**8.3 The one genuinely transform-shaped list is private.**
|
|
649
|
+
`privacy-catalog.js`'s `PRIVACY_TRANSFORM_CALLEES` is a hand-curated set
|
|
650
|
+
already grouped along `TRANSFORM_KINDS` lines — hashes (`createHash`, `sha256`,
|
|
651
|
+
`md5`), KDFs (`bcrypt`, `scrypt`, `argon2`), ciphers (`createCipheriv`,
|
|
652
|
+
`encrypt`, `seal`), and masking (`mask`, `redact`, `anonymize`,
|
|
653
|
+
`pseudonymize`, `tokenize`). It is **not exported**; it is reachable only
|
|
654
|
+
through the `isPrivacyTransformCallee(calleeExpr)` boolean predicate, which
|
|
655
|
+
tells a caller *that* a callee is a transform but never *which kind*. D4 must
|
|
656
|
+
resolve that access problem — the conservative option is a lineage-side table
|
|
657
|
+
of its own, mirroring §6.3's per-entry precedent; exporting the set from
|
|
658
|
+
`privacy-catalog.js` is an additive alternative. D1 records the constraint and
|
|
659
|
+
does not decide it.
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
## 9. Checklist for the follow-up increments
|
|
664
|
+
|
|
665
|
+
### 9.0 The decision shape, and how `category` reaches a node
|
|
666
|
+
|
|
667
|
+
Both registries return the same object:
|
|
668
|
+
`{kind, category, coverageStatus, externality, reason}`.
|
|
669
|
+
|
|
670
|
+
**`category` is the REGISTRY's own field name. On a `DataFlowGraph v1` node it
|
|
671
|
+
becomes `subtype`.** The PRD's §10.3 node contract has no `category` field at
|
|
672
|
+
all — the vocabulary lands in `subtype` ("Framework/provider-specific type such
|
|
673
|
+
as `express-route`, `postgres-table`, `stdout`"), and `schema.js`'s own comment
|
|
674
|
+
on `SOURCE_CATEGORIES`/`SINK_CATEGORIES` says so explicitly: *"the fixed
|
|
675
|
+
vocabulary a node's `subtype`/an inventory row's `category` field draws from."*
|
|
676
|
+
So D2/D3 emit `category`, and Sub-project E's graph builder writes it to
|
|
677
|
+
`node.subtype`. Neither side should invent a third name, and a registry must
|
|
678
|
+
not emit a field literally called `subtype` — the two are the same vocabulary
|
|
679
|
+
at different layers, and conflating them would let a registry decision be
|
|
680
|
+
validated as a node without ever passing through the builder.
|
|
681
|
+
|
|
682
|
+
A `null` `category` therefore becomes a `null`/absent `subtype`, which is
|
|
683
|
+
exactly what an `unsupported` node needs (§6.4) — its `kind` and `reason` carry
|
|
684
|
+
the meaning instead.
|
|
685
|
+
|
|
686
|
+
### D2 — `src/lineage/source-registry.js`
|
|
687
|
+
|
|
688
|
+
1. Export `reclassifySource(entry)` returning the §9.0 shape. `kind` is always
|
|
689
|
+
`'source'` (§7.1). `externality` is derived via `SOURCE_CATEGORY_EXTERNALITY`
|
|
690
|
+
— see §7.5's 2026-08-31 implementation note for the table and the
|
|
691
|
+
counterparty-rule derivation this section's own reasoning did not
|
|
692
|
+
originally cover for the source side.
|
|
693
|
+
2. Port `PROVENANCE_MAP` (12 rows, §4), `AGENT_TOOL_REFINEMENT` (8 rows, §4.1),
|
|
694
|
+
the `language === 'cpp'` descriptor-generic refinement (§4.2) and
|
|
695
|
+
`NO_PROVENANCE_OVERRIDES` (82 rows, §4.3) from
|
|
696
|
+
`test/lineage/registry-mapping-poc.test.js` — see §9.1 for why that file is
|
|
697
|
+
the source of truth for the 82-row table and what happens to it afterwards.
|
|
698
|
+
3. Import `CATALOG` from `../dataflow/catalog.js`. Import nothing else from
|
|
699
|
+
`dataflow/` — no matcher, no engine (§1).
|
|
700
|
+
4. Keep the completeness guards as **shipped** tests, not PoC-only: the
|
|
701
|
+
provenance key set and the override key set must each be asserted equal to
|
|
702
|
+
the live catalog's, both directions. They are the only thing standing
|
|
703
|
+
between a new catalog entry and a silently uncategorized node. All three
|
|
704
|
+
were mutation-proven to fail in D1.
|
|
705
|
+
5. Never emit `manual` (§6.5).
|
|
706
|
+
|
|
707
|
+
### D3 — `src/lineage/sink-registry.js`
|
|
708
|
+
|
|
709
|
+
1. Export `reclassifySink(entry)` and `reclassifyPrivacySink(entry)` — two
|
|
710
|
+
functions, because the two catalogs key on different fields (§2.1) and
|
|
711
|
+
`privacy-catalog.js` is deliberately not merged into `CATALOG`.
|
|
712
|
+
2. Port `CWE_MAP` (20 rows, §5.1), the CWE-79 `framework` refinement (§5.2),
|
|
713
|
+
`PRIVACY_CATEGORY_MAP` (9 rows, §5.3), `CATEGORY_NODE_KIND` and
|
|
714
|
+
`CATEGORY_EXTERNALITY` (§7.1, §7.5).
|
|
715
|
+
3. **Preserve the `unsupported` → `process` node, with its reason string.**
|
|
716
|
+
Dropping those 82 entries would be the single easiest way to violate AC-11
|
|
717
|
+
and FR-201, and it would look like an optimization. D1/3c's biconditional
|
|
718
|
+
and D1/1e's non-empty-reason assertion must both ship.
|
|
719
|
+
4. Carry forward the `thirdPartySdk` match-time refinement as a **known open
|
|
720
|
+
item, not a silent `analytics`** (§5.3): a registry cannot resolve it, but a
|
|
721
|
+
later match-time consumer that knows which receiver matched can, and should
|
|
722
|
+
promote `partial` → `modeled` when it does.
|
|
723
|
+
5. **Close FR-203 — a dynamic destination still produces a node.** The parent
|
|
724
|
+
scoping doc assigns FR-203 to D3 explicitly, and nothing else in this
|
|
725
|
+
checklist covers it, because it is a *different axis* from everything above:
|
|
726
|
+
§5's tables answer "which category is this sink", FR-203 answers "we know
|
|
727
|
+
the call, but not where it points". A recognized sink whose destination
|
|
728
|
+
cannot be resolved (`fetch(url)` with a computed `url`, an SDK client built
|
|
729
|
+
from config) must emit a node with **`kind: 'unresolved'`** — the
|
|
730
|
+
`NODE_KINDS` value that exists for exactly this and is used nowhere else in
|
|
731
|
+
this design — its resolved `category` retained (the *category* is known even
|
|
732
|
+
when the *destination* is not), `externality: 'unknown'`, and a `reason`
|
|
733
|
+
naming the expression that prevented resolution, since FR-203 requires the
|
|
734
|
+
evidence panel to show it. **`coverageStatus`** carries over unchanged from
|
|
735
|
+
whatever the category mapping already assigned the underlying catalog entry
|
|
736
|
+
(§4/§5) — destination resolution is a different axis from classification
|
|
737
|
+
confidence (this item's own opening sentence), so a `modeled` sink whose
|
|
738
|
+
destination happens to be dynamic stays `modeled`; it does NOT demote to
|
|
739
|
+
`partial` the way §16.7 half 2's degraded-dead-end node does (that node
|
|
740
|
+
carries `partial` for an unrelated reason — the analysis itself is
|
|
741
|
+
incomplete, not the classification, per §9.2's own routing above — do not
|
|
742
|
+
conflate the two `unresolved`-kind cases). **Never** drop the node, and never let it
|
|
743
|
+
degrade into the `process`/`unsupported` bucket, which means something
|
|
744
|
+
entirely different (§3: no category exists at all). Note D3 can only mark
|
|
745
|
+
the *shape*; actually resolving destinations is FR-202 and lands in
|
|
746
|
+
Milestone 2 (§7.5).
|
|
747
|
+
6. Ship the measured-count pins (§4.3, §5.1, §5.2) as tests. `bench/layer-recall`
|
|
748
|
+
has already demonstrated in this repo that a floor-only gate lets a stale
|
|
749
|
+
published number survive for weeks; these are equality pins for that reason.
|
|
750
|
+
|
|
751
|
+
### 9.1 Who owns the 82-row override table, and who deletes the PoC
|
|
752
|
+
|
|
753
|
+
Two questions every prior sub-project answered explicitly and this one must
|
|
754
|
+
too (the precedent is `DESIGN_PATH_PROVENANCE.md` §13.7 item 15, where a single
|
|
755
|
+
absorbing task carried an explicit deletion instruction).
|
|
756
|
+
|
|
757
|
+
**The override table's permanent home is `source-registry.js`.** It is code,
|
|
758
|
+
not prose, and it belongs in the module that executes it. This ADR
|
|
759
|
+
deliberately does **not** reproduce all 82 rows: a hand-copied 82-row table in
|
|
760
|
+
markdown would drift from the executable one with nothing able to detect the
|
|
761
|
+
drift, which is strictly worse than a single authoritative copy plus a pointer.
|
|
762
|
+
What this document owns instead is the *derivation rule* (§4.3 + §6.3) and the
|
|
763
|
+
*completeness guarantee* (D2 item 4) — both of which survive any move of the
|
|
764
|
+
table itself. Until D2 lands, the PoC is the table's interim home; after D2
|
|
765
|
+
lands, `source-registry.js` is, and the PoC's copy is redundant.
|
|
766
|
+
|
|
767
|
+
**Deletion: whichever of D2/D3 lands SECOND deletes
|
|
768
|
+
`test/lineage/registry-mapping-poc.test.js`**, in its own commit, after
|
|
769
|
+
confirming the other increment's absorption is complete. D2 and D3 run in
|
|
770
|
+
parallel (§9.2) and each absorbs a *disjoint* half of the PoC — D2 the source
|
|
771
|
+
tables and guards, D3 the sink and privacy ones — so neither may delete it
|
|
772
|
+
unilaterally while the other is still in flight. The second-lander must verify
|
|
773
|
+
both halves are present as shipped tests before removing it, and must also
|
|
774
|
+
remove the file from `package.json`'s `test:lineage` script and from this
|
|
775
|
+
package's `CLAUDE.md` table in the same commit. If D4 somehow lands before
|
|
776
|
+
either, it changes nothing: D4 absorbs no part of this file (§8 is
|
|
777
|
+
confirmations only, `D1/7a`/`D1/7b` stay until the second registry lands).
|
|
778
|
+
|
|
779
|
+
### 9.2 Sequencing and D5's exit criterion
|
|
780
|
+
|
|
781
|
+
D2 and D3 are independent and can run in parallel — they share only this
|
|
782
|
+
document and `schema.js`, and touch disjoint files. D4 depends on §8 but not on
|
|
783
|
+
D2/D3.
|
|
784
|
+
|
|
785
|
+
**D5's exit criterion, as the parent scoping doc states it, cannot pass — and
|
|
786
|
+
must be corrected before D5 is briefed.** That doc's D5 row reads: *"every
|
|
787
|
+
FR-101/FR-201 source/sink category has at least one real-code proof."* §7.2
|
|
788
|
+
measures why that is unachievable: **19 of 29 sink categories and 7 of 21
|
|
789
|
+
source categories have zero catalog entries mapping to them**, by construction
|
|
790
|
+
rather than by any implementation gap. No quality of D2/D3/D4 execution can
|
|
791
|
+
change that, because the detection those categories would need does not exist
|
|
792
|
+
anywhere in the scanner (§7.2's headline: all nine `ai-*` sink categories are
|
|
793
|
+
among them). Briefed as written, D5 would either fail permanently or be quietly
|
|
794
|
+
softened — and quietly softening a coverage criterion is precisely the failure
|
|
795
|
+
mode this whole PRD treats as load-bearing.
|
|
796
|
+
|
|
797
|
+
This is a defect in D5's *paraphrase*, not in FR-201. FR-201's own text says
|
|
798
|
+
**"all *supported* sinks must remain discoverable"**, and the `unsupported`-
|
|
799
|
+
with-reason design satisfies that honestly: every one of the 756 entries
|
|
800
|
+
produces a node carrying a non-empty reason (`D1/1e`), and every unreachable
|
|
801
|
+
category is named in a test that fails if the set changes (`D1/6a`, `D1/6b`).
|
|
802
|
+
|
|
803
|
+
**The corrected criterion D2–D5 should be built and judged against:**
|
|
804
|
+
|
|
805
|
+
> Every **reachable** category has at least one real-code proof, **and** every
|
|
806
|
+
> **unreachable** category has a recorded, tested reason — never silent
|
|
807
|
+
> absence.
|
|
808
|
+
|
|
809
|
+
Both halves are testable today and already have their shape fixed by `D1/6a`
|
|
810
|
+
and `D1/6b`, which pin the unreachable sets as exact lists. D5's job is to
|
|
811
|
+
extend that from "the registry maps it" to "real parsed code exercises it",
|
|
812
|
+
for the reachable half only, while inheriting the unreachable half's existing
|
|
813
|
+
assertions unchanged. A category moving from unreachable to reachable is then
|
|
814
|
+
a *deliberate* event that fails a test and forces both lists to be updated —
|
|
815
|
+
which is the behavior wanted, and the opposite of a floor-only gate.
|
|
816
|
+
|
|
817
|
+
### Carried forward from Sub-project C: §16.7 Finding 2
|
|
818
|
+
|
|
819
|
+
`DESIGN_PATH_PROVENANCE.md` §16.7 left one finding explicitly **unfixed and
|
|
820
|
+
binding on whoever scopes Sub-project D**, with an instruction that D's own
|
|
821
|
+
documents must carry it forward rather than let it be rediscovered. Recording
|
|
822
|
+
it here discharges that obligation; it is **not** resolved by D1, and it is not
|
|
823
|
+
D2's or D3's to resolve either.
|
|
824
|
+
|
|
825
|
+
The finding: a §13.6 context-cap-degraded binding edge's target node is
|
|
826
|
+
unreachable from `path-query.js`'s `sinkCandidates()`. That node has zero
|
|
827
|
+
out-edges and is none of the `return`/`escape`/`loss` kinds `sinkCandidates()`
|
|
828
|
+
enumerates, so a normal **sink-rooted** reconstruction never surfaces it at
|
|
829
|
+
all. `flow-grade.js` grades it correctly if a caller reaches it directly;
|
|
830
|
+
nothing in a sink-rooted query asks. This is §18.4's "truncation must never
|
|
831
|
+
look like no-flow" constraint re-opening at the *query* boundary rather than
|
|
832
|
+
the recording boundary.
|
|
833
|
+
|
|
834
|
+
§16.7 offers two fixes, and they are **not alternatives — they are two halves,
|
|
835
|
+
one of which is D's**. Both are settled here.
|
|
836
|
+
|
|
837
|
+
**Half 2 (the taxonomy) IS D's, and is decided now.** §16.7's second option —
|
|
838
|
+
"Sub-project D's registry deciding a degraded dead end is a reportable
|
|
839
|
+
endpoint" — is a vocabulary question in §6/§7.1's remit and needs no
|
|
840
|
+
`PathStore` at all, so leaving it open would force Sub-project E to invent the
|
|
841
|
+
node's vocabulary while implementing the enumerator. **A truncation-terminal —
|
|
842
|
+
a `path` node with zero out-edges whose in-edges carry a
|
|
843
|
+
`context-cap-degraded` annotation — is a reportable endpoint, and it carries
|
|
844
|
+
`kind: 'unresolved'`, `coverageStatus: 'partial'`, `externality: 'unknown'`,
|
|
845
|
+
with a `reason` naming the context-cap degradation.** The reasoning, in the
|
|
846
|
+
terms §6 and §7.1 already set: `unresolved` is right for the same reason it is
|
|
847
|
+
right for FR-203 (§9's D3 item 5) — the flow is known to exist and its endpoint
|
|
848
|
+
is not knowable, which is exactly what that kind means, and it must **not** be
|
|
849
|
+
`process`/`unsupported`, which asserts the different and false claim that no
|
|
850
|
+
category models it; `partial` is right because the analyzer genuinely observed
|
|
851
|
+
this flow and lost only its continuation, which is §6.2's "sure, and lossy in a
|
|
852
|
+
stated way", not §6.3's "inferred" or §6.4's "unmodellable"; `externality:
|
|
853
|
+
'unknown'` follows §7.5 directly, since the callee's body was never analyzed
|
|
854
|
+
and nothing is known about where the data goes next. This is the one place in
|
|
855
|
+
this design where a node's `coverageStatus` comes from the *analysis* rather
|
|
856
|
+
than from a catalog entry — consistent, because a truncation-terminal has no
|
|
857
|
+
catalog entry behind it at all, which is exactly why §4/§5's tables could not
|
|
858
|
+
settle it.
|
|
859
|
+
|
|
860
|
+
**Half 1 (the enumerator) is Sub-project E's, not D2's or D3's.**
|
|
861
|
+
`sinkCandidates()` is documented as a
|
|
862
|
+
**registry stand-in** — "there is no sink registry yet — Sub-project D" — so
|
|
863
|
+
the obvious reading is that D3 replaces it and inherits the bug. That reading
|
|
864
|
+
is wrong. D3 produces a *classification for a catalog entry*; it does not
|
|
865
|
+
enumerate graph nodes, and it never sees a `PathStore`. The consumer that
|
|
866
|
+
eventually replaces `sinkCandidates()` with real registry-backed sink
|
|
867
|
+
enumeration is **Sub-project E's graph builder**, and that is where the fix
|
|
868
|
+
belongs — either by adding the candidate "truncation-terminal" node shape §16.7
|
|
869
|
+
sketches, or by having the enumerator union `diagnostics()`'s degraded targets
|
|
870
|
+
into its candidate set. Whoever scopes Sub-project E must pick one up front,
|
|
871
|
+
because a registry-backed enumerator that simply mirrors `sinkCandidates()`'
|
|
872
|
+
current kind list reproduces the gap exactly.
|
|
873
|
+
|
|
874
|
+
### Not in scope for any of D2/D3/D4
|
|
875
|
+
|
|
876
|
+
Modifying `catalog.js` / `catalog-expanded.js` / `privacy-catalog.js` /
|
|
877
|
+
`schema.js` (§7.4); FR-202 destination resolution; any `DataFlowGraph v1`
|
|
878
|
+
envelope output or graph builder (Sub-project E); wiring any registry into
|
|
879
|
+
`runScan`; new *detection* of any kind — in particular the AI-destination gap
|
|
880
|
+
(§7.2), which reclassification cannot close.
|