@clear-capabilities/agentic-security-scanner 0.145.0 → 0.147.5

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.
Files changed (195) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/bin/agentic-security.js +3508 -69
  3. package/dist/1122.index.js +779 -0
  4. package/dist/{301.index.js → 1301.index.js} +2 -2
  5. package/dist/1379.index.js +591 -0
  6. package/dist/{444.index.js → 1444.index.js} +2 -2
  7. package/dist/{660.index.js → 1660.index.js} +2 -2
  8. package/dist/{700.index.js → 1700.index.js} +2 -2
  9. package/dist/{905.index.js → 1905.index.js} +3 -3
  10. package/dist/{920.index.js → 1920.index.js} +3 -3
  11. package/dist/{238.index.js → 2238.index.js} +3 -3
  12. package/dist/{271.index.js → 2271.index.js} +2 -2
  13. package/dist/{985.index.js → 2376.index.js} +1260 -340
  14. package/dist/2432.index.js +793 -0
  15. package/dist/2659.index.js +93 -0
  16. package/dist/{826.index.js → 2826.index.js} +2 -2
  17. package/dist/{830.index.js → 2830.index.js} +2 -2
  18. package/dist/2923.index.js +298 -0
  19. package/dist/{1.index.js → 3001.index.js} +5 -5
  20. package/dist/{117.index.js → 3117.index.js} +3 -3
  21. package/dist/3180.index.js +379 -0
  22. package/dist/3276.index.js +117 -0
  23. package/dist/{415.index.js → 3415.index.js} +2 -2
  24. package/dist/{499.index.js → 3499.index.js} +2 -2
  25. package/dist/3518.index.js +450 -0
  26. package/dist/{526.index.js → 3526.index.js} +5 -5
  27. package/dist/{736.index.js → 3736.index.js} +4 -4
  28. package/dist/{839.index.js → 3839.index.js} +4 -4
  29. package/dist/{113.index.js → 4113.index.js} +5 -5
  30. package/dist/{265.index.js → 4265.index.js} +2 -2
  31. package/dist/{384.index.js → 4384.index.js} +3 -3
  32. package/dist/4547.index.js +268 -0
  33. package/dist/4863.index.js +422 -0
  34. package/dist/{970.index.js → 4970.index.js} +2 -2
  35. package/dist/5051.index.js +841 -0
  36. package/dist/{144.index.js → 5144.index.js} +5 -5
  37. package/dist/{333.index.js → 5333.index.js} +3 -3
  38. package/dist/5343.index.js +185 -0
  39. package/dist/5350.index.js +866 -0
  40. package/dist/5561.index.js +436 -0
  41. package/dist/{637.index.js → 5637.index.js} +3 -3
  42. package/dist/{449.index.js → 5830.index.js} +2 -2
  43. package/dist/6626.index.js +532 -0
  44. package/dist/6662.index.js +297 -0
  45. package/dist/{675.index.js → 6675.index.js} +5 -5
  46. package/dist/{730.index.js → 6730.index.js} +6 -6
  47. package/dist/6829.index.js +225 -0
  48. package/dist/6944.index.js +130 -0
  49. package/dist/{178.index.js → 7178.index.js} +3 -3
  50. package/dist/{227.index.js → 7227.index.js} +2 -2
  51. package/dist/7310.index.js +520 -0
  52. package/dist/{552.index.js → 7552.index.js} +4 -4
  53. package/dist/7709.index.js +78 -0
  54. package/dist/8218.index.js +160 -0
  55. package/dist/{476.index.js → 8476.index.js} +4 -4
  56. package/dist/{513.index.js → 8513.index.js} +5 -5
  57. package/dist/{520.index.js → 8520.index.js} +2 -2
  58. package/dist/{718.index.js → 8718.index.js} +2 -2
  59. package/dist/{752.index.js → 8752.index.js} +2 -2
  60. package/dist/8846.index.js +100 -0
  61. package/dist/{435.index.js → 9091.index.js} +560 -189
  62. package/dist/{207.index.js → 9207.index.js} +2 -2
  63. package/dist/{220.index.js → 9220.index.js} +2 -2
  64. package/dist/9390.index.js +163 -0
  65. package/dist/{503.index.js → 9503.index.js} +2 -2
  66. package/dist/{801.index.js → 9801.index.js} +2 -2
  67. package/dist/{824.index.js → 9824.index.js} +2 -2
  68. package/dist/agentic-security.mjs +16 -16
  69. package/dist/agentic-security.mjs.sha256 +1 -1
  70. package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
  71. package/dist/frontend/index.html +21 -0
  72. package/dist/frontend/src/app.js +176 -0
  73. package/dist/frontend/src/components/evidence-inspector.js +141 -0
  74. package/dist/frontend/src/components/filter-rail.js +119 -0
  75. package/dist/frontend/src/components/query-bar.js +126 -0
  76. package/dist/frontend/src/data/flagship-graph.js +1460 -0
  77. package/dist/frontend/src/export-entry.js +36 -0
  78. package/dist/frontend/src/lib/api-client.js +92 -0
  79. package/dist/frontend/src/lib/contrast.js +34 -0
  80. package/dist/frontend/src/lib/dom.js +24 -0
  81. package/dist/frontend/src/lib/escape-html.js +16 -0
  82. package/dist/frontend/src/lib/flow-path.js +40 -0
  83. package/dist/frontend/src/lib/focus-controls.js +149 -0
  84. package/dist/frontend/src/lib/protection-visual.js +46 -0
  85. package/dist/frontend/src/lib/query-language.js +240 -0
  86. package/dist/frontend/src/lib/row-filters.js +43 -0
  87. package/dist/frontend/src/lib/state.js +84 -0
  88. package/dist/frontend/src/main.js +83 -0
  89. package/dist/frontend/src/shell.js +184 -0
  90. package/dist/frontend/src/views/architecture-view.js +798 -0
  91. package/dist/frontend/src/views/inventory-view.js +292 -0
  92. package/dist/frontend/src/views/privacy-view.js +172 -0
  93. package/dist/frontend/src/views/trace-view.js +206 -0
  94. package/dist/frontend/styles/architecture-view.css +93 -0
  95. package/dist/frontend/styles/filter-rail.css +34 -0
  96. package/dist/frontend/styles/inspector.css +69 -0
  97. package/dist/frontend/styles/inventory-view.css +74 -0
  98. package/dist/frontend/styles/privacy-view.css +86 -0
  99. package/dist/frontend/styles/query-bar.css +107 -0
  100. package/dist/frontend/styles/shell.css +155 -0
  101. package/dist/frontend/styles/tokens.css +128 -0
  102. package/dist/frontend/styles/trace-view.css +95 -0
  103. package/package.json +13 -6
  104. package/src/dataflow/CLAUDE.md +1 -1
  105. package/src/dataflow/catalog.js +42 -0
  106. package/src/dataflow/orm-write-catalog.js +175 -0
  107. package/src/engine.js +92 -1
  108. package/src/ir/CLAUDE.md +1 -0
  109. package/src/ir/chrome-probe.mjs +150 -0
  110. package/src/ir/parser-js.js +94 -7
  111. package/src/lineage/CLAUDE.md +1203 -0
  112. package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
  113. package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
  114. package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
  115. package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
  116. package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
  117. package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
  118. package/src/lineage/DESIGN_REGISTRIES.md +880 -0
  119. package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
  120. package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
  121. package/src/lineage/classification.js +56 -0
  122. package/src/lineage/coverage.js +658 -0
  123. package/src/lineage/cross-repo-link.js +107 -0
  124. package/src/lineage/dataflow-graph.schema.json +184 -0
  125. package/src/lineage/decision-story.js +206 -0
  126. package/src/lineage/drift-policy.js +279 -0
  127. package/src/lineage/driver.js +135 -0
  128. package/src/lineage/engine.js +992 -0
  129. package/src/lineage/export-briefing.js +628 -0
  130. package/src/lineage/export-csv.js +62 -0
  131. package/src/lineage/export-json.js +238 -0
  132. package/src/lineage/export-privacy.js +258 -0
  133. package/src/lineage/federation-loader.js +111 -0
  134. package/src/lineage/field-identity.js +78 -0
  135. package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
  136. package/src/lineage/fixtures/flagship-graph.json +1453 -0
  137. package/src/lineage/flow-grade.js +221 -0
  138. package/src/lineage/governance-edit.js +169 -0
  139. package/src/lineage/graph-builder.js +1114 -0
  140. package/src/lineage/graph-diff.js +431 -0
  141. package/src/lineage/graph-snapshot.js +180 -0
  142. package/src/lineage/handling-analyzer.js +168 -0
  143. package/src/lineage/ids.js +349 -0
  144. package/src/lineage/impact-assessment.js +76 -0
  145. package/src/lineage/impact-engine.js +268 -0
  146. package/src/lineage/index.js +281 -0
  147. package/src/lineage/language-coverage-tiers.js +58 -0
  148. package/src/lineage/obligation-mapping.js +126 -0
  149. package/src/lineage/obligation-predicates.js +235 -0
  150. package/src/lineage/observation-adapters.js +282 -0
  151. package/src/lineage/observation-correlation.js +622 -0
  152. package/src/lineage/observation-store.js +497 -0
  153. package/src/lineage/path-query.js +410 -0
  154. package/src/lineage/path-store.js +400 -0
  155. package/src/lineage/protection.js +53 -0
  156. package/src/lineage/recipient-profile.js +192 -0
  157. package/src/lineage/recipient-registry.js +394 -0
  158. package/src/lineage/redact-graph.js +224 -0
  159. package/src/lineage/remediation.js +417 -0
  160. package/src/lineage/resolve-destination.js +91 -0
  161. package/src/lineage/runtime-observation.js +464 -0
  162. package/src/lineage/scenario-diff.js +84 -0
  163. package/src/lineage/scenario-engine.js +251 -0
  164. package/src/lineage/scenario.js +101 -0
  165. package/src/lineage/schema.js +167 -0
  166. package/src/lineage/sink-registry.js +427 -0
  167. package/src/lineage/source-registry.js +357 -0
  168. package/src/lineage/source-seeding.js +212 -0
  169. package/src/lineage/summaries.js +590 -0
  170. package/src/lineage/transform-catalog.js +397 -0
  171. package/src/lineage/transit-protection.js +150 -0
  172. package/src/lineage/validate.js +285 -0
  173. package/src/mcp/CLAUDE.md +7 -1
  174. package/src/mcp/dataflow-tools.js +160 -0
  175. package/src/mcp/server.js +1 -1
  176. package/src/mcp/tools.js +2 -1
  177. package/src/pipeline/scan-health.js +19 -1
  178. package/src/posture/CLAUDE.md +19 -0
  179. package/src/posture/artifact-registry.js +52 -0
  180. package/src/posture/auditor-walkthrough.js +76 -0
  181. package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
  182. package/src/posture/obligation-evidence-pack.js +202 -0
  183. package/src/posture/remediation-ledger.js +337 -0
  184. package/src/server/CLAUDE.md +47 -0
  185. package/src/server/graph-loader.js +141 -0
  186. package/src/server/http-server.js +325 -0
  187. package/src/server/routes.js +129 -0
  188. package/src/server/security.js +111 -0
  189. package/src/server/static-assets.js +144 -0
  190. package/src/shared/frontend-root.js +52 -0
  191. package/dist/11.index.js +0 -353
  192. package/dist/259.index.js +0 -975
  193. package/dist/317.index.js +0 -300
  194. package/dist/609.index.js +0 -741
  195. package/dist/838.index.js +0 -152
@@ -0,0 +1,107 @@
1
+ // cross-repo-link.js — M5 deliverable #8 (FR-304's "declared" half only,
2
+ // per the M5 top-level scoping doc's own DFG-025 row and this
3
+ // deliverable's own scoping doc, 2026-09-02). The CrossRepoLink
4
+ // extension contract — a graph-attached array (mirrors
5
+ // `graph.recipientProfiles[]`'s own precedent, `recipient-profile.js`),
6
+ // NEVER a DataFlowGraph v1 core-schema edge: `validate.js`'s
7
+ // `_validateEdge` requires both endpoints of an edge to resolve against
8
+ // the ONE graph's own `nodeIds` set, so a foreign node id from a
9
+ // different repo's build can never pass `validateGraph()` — settling,
10
+ // not merely motivating, the decision that a cross-repo link must be a
11
+ // separate extension record.
12
+ //
13
+ // Mirrors `recipient-profile.js`'s own file shape exactly (pure module,
14
+ // `{valid, errors}` validator, zero graph access at construction time),
15
+ // with the one real, disclosed departure that module's own header also
16
+ // discloses for itself: no per-field `fieldEvidence` map, since every
17
+ // field on a CrossRepoLink is uniformly operator-declared (no
18
+ // code-derived half) — closer to `ObligationMapping`'s single
19
+ // record-level `factType` shape (here, `provenance`) than to
20
+ // `RecipientProfile`'s per-field one.
21
+ //
22
+ // `provenance` reuses `schema.js`'s own `EDGE_PROVENANCE_VALUES` — this
23
+ // deliverable's CLI is the FIRST real producer of `'manual'` anywhere in
24
+ // this codebase (confirmed by the scoping investigation: every shipped
25
+ // edge is `provenance: 'code'`, unconditionally, per Milestone 2
26
+ // Sub-project F increment 1). `'schema'` stays reserved on the SAME
27
+ // field for a future "imported"/auto-correlated producer (FR-304's
28
+ // second flavor — destination/schema-based automatic cross-repo edge
29
+ // correlation) — explicitly out of scope for this deliverable, per the
30
+ // scoping doc's own "The real correction" section.
31
+
32
+ import { EDGE_PROVENANCE_VALUES } from './schema.js';
33
+
34
+ export const CROSS_REPO_LINK_VERSION = '1.0.0';
35
+
36
+ // The operator-config filename this deliverable's CLI reads/writes,
37
+ // resolved via `posture/state-dir.js`'s `statePath()` — mirrors
38
+ // `recipient-registry.js`'s own `RECIPIENT_CONFIG_FILENAME` precedent.
39
+ export const CROSS_REPO_LINKS_FILENAME = 'cross-repo-links.json';
40
+
41
+ // Fixed, single legal value — mirrors `edge.relationship`'s own single
42
+ // legal value ('data_flow', validate.js's `_validateEdge`). No new
43
+ // taxonomy is introduced for this deliverable.
44
+ export const CROSS_REPO_LINK_RELATIONSHIP = 'data_flow';
45
+
46
+ function _isNonEmptyString(v) { return typeof v === 'string' && v.length > 0; }
47
+ function _isStringOrNull(v) { return v === null || v === undefined || typeof v === 'string'; }
48
+ function _isPlainObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }
49
+
50
+ // Shared endpoint-shape check for `local`/`remote` — `local` always
51
+ // checks {graphId, graphDigest, nodeId}; `remote` additionally checks
52
+ // {repository, sourceFile} via `extraFields`.
53
+ function _validateEndpoint(endpoint, label, err, extraFields = []) {
54
+ if (!_isPlainObject(endpoint)) {
55
+ err(`$.${label}`, `${label} is required and must be an object`);
56
+ return;
57
+ }
58
+ if (!_isNonEmptyString(endpoint.graphId)) err(`$.${label}.graphId`, `${label}.graphId is required`);
59
+ if (!_isNonEmptyString(endpoint.graphDigest)) err(`$.${label}.graphDigest`, `${label}.graphDigest is required`);
60
+ if (!_isNonEmptyString(endpoint.nodeId)) err(`$.${label}.nodeId`, `${label}.nodeId is required`);
61
+ for (const field of extraFields) {
62
+ if (!_isNonEmptyString(endpoint[field])) err(`$.${label}.${field}`, `${label}.${field} is required`);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Structural validation only — mirrors `validateRecipientProfile`'s/
68
+ * `validateScenario`'s own `{valid, errors}` shape and "never throws"
69
+ * contract. Never confirms `local.nodeId`/`remote.nodeId` actually exist
70
+ * in any real graph — that needs real graph content, which this pure
71
+ * module deliberately has no access to (mirrors `scenario.js`'s own
72
+ * "structural-only, zero graph access" boundary exactly). That check is
73
+ * `federation-loader.js`'s (for the remote side) and the CLI's own
74
+ * `loadSignedGraph` call (for the local side) job, at declare time.
75
+ *
76
+ * @param {object} record
77
+ * @returns {{valid: boolean, errors: Array<{path: string, message: string}>}}
78
+ */
79
+ export function validateCrossRepoLink(record) {
80
+ const errors = [];
81
+ const err = (p, message) => errors.push({ path: p, message });
82
+
83
+ if (!_isPlainObject(record)) {
84
+ err('$', 'CrossRepoLink record must be an object');
85
+ return { valid: false, errors };
86
+ }
87
+
88
+ if (!_isNonEmptyString(record.id) || !record.id.startsWith('crosslink:')) {
89
+ err('$.id', 'id is required and must start with "crosslink:"');
90
+ }
91
+ if (!_isNonEmptyString(record.version)) err('$.version', 'version is required');
92
+ if (!EDGE_PROVENANCE_VALUES.includes(record.provenance)) {
93
+ err('$.provenance', `unrecognized provenance "${record.provenance}" — must be one of ${EDGE_PROVENANCE_VALUES.join('|')}`);
94
+ }
95
+ if (record.relationship !== CROSS_REPO_LINK_RELATIONSHIP) {
96
+ err('$.relationship', `relationship must be "${CROSS_REPO_LINK_RELATIONSHIP}" (got "${record.relationship}")`);
97
+ }
98
+
99
+ _validateEndpoint(record.local, 'local', err);
100
+ _validateEndpoint(record.remote, 'remote', err, ['repository', 'sourceFile']);
101
+
102
+ if (!_isStringOrNull(record.rationale)) err('$.rationale', 'rationale must be a string or null');
103
+ if (!_isNonEmptyString(record.declaredBy)) err('$.declaredBy', 'declaredBy is required');
104
+ if (!_isNonEmptyString(record.declaredAt)) err('$.declaredAt', 'declaredAt is required');
105
+
106
+ return { valid: errors.length === 0, errors };
107
+ }
@@ -0,0 +1,184 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://agentic-security.dev/schemas/dataflow-graph/1.0.0.json",
4
+ "title": "DataFlowGraph v1",
5
+ "description": "Canonical application data-lineage graph contract (Data Flow Explorer PRD section 10). Renderer-independent; consumed by Architecture, Privacy, and Trace projections.",
6
+ "type": "object",
7
+ "required": ["schemaVersion", "graphId", "generatedAt", "scope", "scanHealth", "taxonomy", "nodes", "edges", "dataElements", "transformations", "flows", "controls", "policies", "evidence", "coverage", "limitations", "extensions"],
8
+ "properties": {
9
+ "schemaVersion": { "const": "1.0.0" },
10
+ "graphId": { "type": "string", "pattern": "^dfg:" },
11
+ "generatedAt": { "type": "string", "format": "date-time" },
12
+ "scope": {
13
+ "type": "object",
14
+ "properties": {
15
+ "source": { "type": "string", "enum": ["scan", "fixture"] }
16
+ }
17
+ },
18
+ "scanHealth": { "type": "object" },
19
+ "taxonomy": { "type": "object" },
20
+ "nodes": { "type": "array", "items": { "$ref": "#/$defs/node" } },
21
+ "edges": { "type": "array", "items": { "$ref": "#/$defs/edge" } },
22
+ "dataElements": { "type": "array", "items": { "$ref": "#/$defs/dataElement" } },
23
+ "transformations": { "type": "array", "items": { "$ref": "#/$defs/transformation" } },
24
+ "flows": { "type": "array", "items": { "$ref": "#/$defs/flow" } },
25
+ "controls": { "type": "array" },
26
+ "policies": { "type": "array" },
27
+ "evidence": { "type": "array", "items": { "$ref": "#/$defs/evidence" } },
28
+ "coverage": { "type": "object" },
29
+ "limitations": { "type": "array" },
30
+ "extensions": { "type": "object" }
31
+ },
32
+ "$defs": {
33
+ "node": {
34
+ "type": "object",
35
+ "required": ["id", "kind", "label", "aliases", "system", "externality", "lifecycleStages", "governanceRefs", "dataElementIds", "evidenceRefs", "confidence", "coverageStatus"],
36
+ "properties": {
37
+ "id": { "type": "string", "pattern": "^node:" },
38
+ "kind": { "type": "string", "enum": ["source", "process", "transform", "api", "store", "queue", "log", "sink", "external", "boundary", "unresolved"] },
39
+ "subtype": { "type": ["string", "null"] },
40
+ "label": { "type": "string" },
41
+ "aliases": { "type": "array", "items": { "type": "string" } },
42
+ "location": { "type": ["object", "null"] },
43
+ "system": { "type": "object" },
44
+ "destination": { "type": ["object", "null"] },
45
+ "externality": {
46
+ "type": "object",
47
+ "properties": {
48
+ "value": { "type": "string", "enum": ["internal", "external", "unknown"] },
49
+ "evidenceRefs": { "type": "array", "items": { "type": "string" } }
50
+ }
51
+ },
52
+ "lifecycleStages": { "type": "array", "items": { "type": "string" } },
53
+ "governanceRefs": { "type": "object" },
54
+ "dataElementIds": { "type": "array", "items": { "type": "string" } },
55
+ "evidenceRefs": { "type": "array", "items": { "type": "string" } },
56
+ "confidence": { "type": "object" },
57
+ "coverageStatus": { "type": "string", "enum": ["modeled", "partial", "candidate", "unsupported", "manual"] }
58
+ }
59
+ },
60
+ "fieldMapping": {
61
+ "type": "object",
62
+ "required": ["fromPath", "toPath", "dataElementIds", "mappingType", "transformationIds"],
63
+ "properties": {
64
+ "fromPath": { "type": "string" },
65
+ "toPath": { "type": "string" },
66
+ "dataElementIds": { "type": "array", "items": { "type": "string" } },
67
+ "mappingType": { "type": "string", "enum": ["identity", "rename", "projection", "serialization", "deserialization", "transformation", "aggregation", "join", "filter", "sort", "conditional", "unknown"] },
68
+ "transformationIds": { "type": "array", "items": { "type": "string" } }
69
+ }
70
+ },
71
+ "protocol": {
72
+ "type": "object",
73
+ "properties": {
74
+ "name": { "type": "string" },
75
+ "destinationResolution": { "type": "string", "enum": ["literal", "resolved_from_constant", "resolved_from_config", "resolved_from_schema", "declared_service", "runtime_corroborated", "dynamic", "unknown"] }
76
+ }
77
+ },
78
+ "protectionDimension": {
79
+ "type": "object",
80
+ "required": ["verdict", "evidenceGrade"],
81
+ "properties": {
82
+ "verdict": { "type": "string", "enum": ["protected", "unprotected", "unknown", "not_applicable", "not_assessed"] },
83
+ "evidenceGrade": { "type": "string", "enum": ["runtime", "code_and_config", "code", "config", "declared", "assumed", "manual", "none"] }
84
+ }
85
+ },
86
+ "edge": {
87
+ "type": "object",
88
+ "required": ["id", "from", "to", "relationship", "fieldMappings", "protocol", "boundaryCrossings", "provenance", "protection", "evidenceRefs", "coverageStatus"],
89
+ "properties": {
90
+ "id": { "type": "string", "pattern": "^edge:" },
91
+ "from": { "type": "string" },
92
+ "to": { "type": "string" },
93
+ "relationship": { "const": "data_flow" },
94
+ "fieldMappings": { "type": "array", "items": { "$ref": "#/$defs/fieldMapping" } },
95
+ "protocol": { "$ref": "#/$defs/protocol" },
96
+ "boundaryCrossings": { "type": "array", "items": { "type": "string" } },
97
+ "provenance": { "type": "string", "enum": ["code", "schema", "manual", "runtime"] },
98
+ "protection": {
99
+ "type": "object",
100
+ "required": ["transit", "atRest", "handling"],
101
+ "properties": {
102
+ "transit": { "$ref": "#/$defs/protectionDimension" },
103
+ "atRest": { "$ref": "#/$defs/protectionDimension" },
104
+ "handling": { "$ref": "#/$defs/protectionDimension" }
105
+ }
106
+ },
107
+ "evidenceRefs": { "type": "array", "items": { "type": "string" } },
108
+ "coverageStatus": { "type": "string", "enum": ["modeled", "partial", "candidate", "unsupported", "manual"] }
109
+ }
110
+ },
111
+ "dataElement": {
112
+ "type": "object",
113
+ "required": ["id", "name", "aliases", "dataClasses", "aiContexts", "sourceLocations", "classificationEvidence", "manualOverride"],
114
+ "properties": {
115
+ "id": { "type": "string", "pattern": "^data:" },
116
+ "name": { "type": "string" },
117
+ "aliases": { "type": "array", "items": { "type": "string" } },
118
+ "declaredType": { "type": ["string", "null"] },
119
+ "dataClasses": { "type": "array", "items": { "type": "string", "enum": ["PII", "PHI", "PCI", "FIN", "CREDENTIALS", "GEOLOCATION", "DEVICE_ID", "CONFIDENTIAL"] } },
120
+ "aiContexts": { "type": "array", "items": { "type": "string", "enum": ["ai.system_prompt", "ai.user_prompt", "ai.model_input", "ai.model_output", "ai.rag_context", "ai.embedding", "ai.vector_store", "ai.memory", "ai.tool_argument", "ai.tool_result", "ai.training_data", "ai.fine_tuning_data", "ai.evaluation_data", "ai.telemetry", "ai.model_artifact"] } },
121
+ "sourceLocations": { "type": "array" },
122
+ "dataSubjectCategory": { "type": ["string", "null"] },
123
+ "classificationEvidence": { "type": "array" },
124
+ "manualOverride": { "type": "boolean" },
125
+ "firstSeenProvenance": { "type": ["object", "null"] }
126
+ }
127
+ },
128
+ "transformation": {
129
+ "type": "object",
130
+ "required": ["id", "kind", "reversibility"],
131
+ "properties": {
132
+ "id": { "type": "string", "pattern": "^transform:" },
133
+ "inputPath": { "type": "string" },
134
+ "outputPath": { "type": "string" },
135
+ "callee": { "type": "string" },
136
+ "location": { "type": ["object", "null"] },
137
+ "kind": { "type": "string", "enum": ["mask", "redact", "tokenize", "hash", "encrypt", "decrypt", "encode", "decode", "aggregate", "truncate", "normalize", "custom", "unknown"] },
138
+ "reversibility": { "type": "string", "enum": ["reversible", "irreversible", "unknown"] },
139
+ "algorithm": { "type": ["string", "null"] },
140
+ "appliesToAllPaths": { "type": ["boolean", "null"] },
141
+ "controlCredit": { "type": ["boolean", "null"] },
142
+ "controlCreditReason": { "type": ["string", "null"] }
143
+ }
144
+ },
145
+ "flow": {
146
+ "type": "object",
147
+ "required": ["id", "dataElementIds", "source", "sink", "edgeIds", "policyVerdict", "protectionSummary", "evidenceRefs", "coverageStatus"],
148
+ "properties": {
149
+ "id": { "type": "string", "pattern": "^flow:" },
150
+ "dataElementIds": { "type": "array", "items": { "type": "string" } },
151
+ "source": { "type": "string" },
152
+ "sink": { "type": "string" },
153
+ "edgeIds": { "type": "array", "items": { "type": "string" } },
154
+ "transformationIds": { "type": "array", "items": { "type": "string" } },
155
+ "alternatePathCount": { "type": "integer", "minimum": 0 },
156
+ "policyVerdict": { "type": "string", "enum": ["prohibited", "permitted", "conditionally_permitted", "manual_review_required", "not_evaluated"] },
157
+ "protectionSummary": { "type": "string", "enum": ["protected", "unprotected", "mixed", "unknown", "not_assessed"] },
158
+ "evidenceRefs": { "type": "array", "items": { "type": "string" } },
159
+ "confidence": { "type": "object" },
160
+ "coverageStatus": { "type": "string", "enum": ["modeled", "partial", "candidate", "unsupported", "manual"] },
161
+ "findingRefs": { "type": "array" },
162
+ "governanceRefs": { "type": "object" },
163
+ "limitations": { "type": "array" }
164
+ }
165
+ },
166
+ "evidence": {
167
+ "type": "object",
168
+ "required": ["id", "claim", "evidenceType"],
169
+ "properties": {
170
+ "id": { "type": "string", "pattern": "^evidence:" },
171
+ "claim": { "type": "string" },
172
+ "evidenceType": { "type": "string", "enum": ["code", "ir", "configuration", "iac", "schema", "service_declaration", "policy", "manual", "runtime"] },
173
+ "location": { "type": ["object", "null"] },
174
+ "producer": { "type": ["string", "null"] },
175
+ "confidenceTier": { "type": ["string", "null"] },
176
+ "snippet": { "type": ["string", "null"] },
177
+ "timestamp": { "type": ["string", "null"] },
178
+ "commit": { "type": ["string", "null"] },
179
+ "limitations": { "type": "array" },
180
+ "conflict": { "type": ["boolean", "null"] }
181
+ }
182
+ }
183
+ }
184
+ }
@@ -0,0 +1,206 @@
1
+ // decision-story.js — M4 deliverable #7 (FR-501 §14, DFG-035): the
2
+ // DecisionStory extension contract (§10.10) + a transparent 9-factor
3
+ // ranking engine over real flows.
4
+ //
5
+ // Mirrors obligation-mapping.js's own shape exactly: a record is
6
+ // explicitly NOT a DataFlowGraph v1 entity (§10.10 — "associated with,
7
+ // but not required inside, the immutable base graph"), never added to
8
+ // dataflow-graph.schema.json, never routed through validate.js.
9
+ //
10
+ // Ranking-factor availability, grounded in real investigation (see this
11
+ // sub-project's own scoping doc): 7 of 9 factors are direct reads off
12
+ // the graph, 1 (breadth) is a small new aggregation defined here, and 2
13
+ // (recipientJurisdiction, changeRecency) are honestly `available: false`
14
+ // on every flow — never fabricated, never silently dropped from the
15
+ // factor list. recipientJurisdiction needs a RecipientProfile extension
16
+ // (capability #6, not yet built); changeRecency needs GraphSnapshot/
17
+ // GraphDiff (capability #3, Data-Flow Time Machine, not yet built).
18
+ //
19
+ // PRD's own binding constraint: "never represents an uncalibrated score
20
+ // as expected financial loss or breach probability." This module NEVER
21
+ // blends factors into a single opaque float — rankFlows performs a
22
+ // documented, transparent, CONFIGURABLE (opts.factorOrder) multi-key
23
+ // sort over each factor's own ordinal tier. Every factor stays
24
+ // individually inspectable on the returned record.
25
+
26
+ import { SEVERITY_RANK, DEFAULT_TAXONOMY } from '../dataflow/privacy-taxonomy.js';
27
+
28
+ // The sink-registry categories (sink-registry.js's own CATEGORY_NODE_KIND
29
+ // vocabulary) that count as an AI destination for the aiUse factor.
30
+ // Exported so export-briefing.js's own "AI providers" chapter-3 listing
31
+ // reads the SAME vocabulary rather than keeping a second, independently
32
+ // drifting copy (Task 2 review finding, fixed).
33
+ export const AI_SINK_SUBTYPES = Object.freeze(['ai-model-provider', 'ai-agent', 'ai-tool']);
34
+
35
+ export const RANKING_FACTORS = Object.freeze([
36
+ 'sensitivity', 'externality', 'controlVerdict', 'recipientJurisdiction',
37
+ 'aiUse', 'breadth', 'evidenceConfidence', 'policyState', 'changeRecency',
38
+ ]);
39
+
40
+ // Ordinal tier ranks, worst-first, used both to score a flow's own
41
+ // factor and to compare two flows during rankFlows's own sort. Every
42
+ // factor uses a SMALL, real, disclosed vocabulary — never a blended
43
+ // number.
44
+ const _TIER_RANK = {
45
+ sensitivity: { critical: 4, high: 3, medium: 2, low: 1, none: 0 },
46
+ externality: { external: 2, unknown: 1, internal: 0 },
47
+ controlVerdict: { unprotected: 4, mixed: 3, unknown: 2, not_assessed: 1, protected: 0, not_applicable: 0 },
48
+ recipientJurisdiction: { unknown: 0 },
49
+ aiUse: { ai_destination: 1, none: 0 },
50
+ breadth: { high: 2, medium: 1, low: 0 },
51
+ // Lower confidence is MORE attention-worthy (an uncertain flow needs
52
+ // review), so the tier rank is inverted relative to the raw score —
53
+ // disclosed here, not left implicit.
54
+ evidenceConfidence: { low: 2, medium: 1, high: 0 },
55
+ policyState: { prohibited: 4, manual_review_required: 3, conditionally_permitted: 2, not_evaluated: 1, permitted: 0 },
56
+ changeRecency: { unknown: 0 },
57
+ };
58
+
59
+ function _isNonEmptyString(v) { return typeof v === 'string' && v.length > 0; }
60
+ function _isPlainObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }
61
+ function _isArray(v) { return Array.isArray(v); }
62
+
63
+ /**
64
+ * Structural validation only — mirrors
65
+ * obligation-mapping.js#validateObligationMapping's own {valid, errors}
66
+ * shape and "never throws" contract.
67
+ */
68
+ export function validateDecisionStory(record) {
69
+ const errors = [];
70
+ const err = (path, message) => errors.push({ path, message });
71
+
72
+ if (!record || typeof record !== 'object' || Array.isArray(record)) {
73
+ err('$', 'DecisionStory record must be an object');
74
+ return { valid: false, errors };
75
+ }
76
+ if (!_isNonEmptyString(record.id) || !record.id.startsWith('story:')) {
77
+ err('$.id', 'id is required and must start with "story:"');
78
+ }
79
+ if (!_isNonEmptyString(record.version)) err('$.version', 'version is required');
80
+ if (!_isNonEmptyString(record.audienceMode)) err('$.audienceMode', 'audienceMode is required');
81
+ if (!_isPlainObject(record.scopeQuery)) err('$.scopeQuery', 'scopeQuery is required and must be an object');
82
+ if (!_isArray(record.chapters)) err('$.chapters', 'chapters must be an array');
83
+ if (!_isArray(record.contributingGraphIds)) err('$.contributingGraphIds', 'contributingGraphIds must be an array');
84
+ if (!_isArray(record.rankingFactors)) err('$.rankingFactors', 'rankingFactors must be an array');
85
+ if (!_isNonEmptyString(record.evidenceGrade)) err('$.evidenceGrade', 'evidenceGrade is required');
86
+ if (!_isPlainObject(record.coverage)) err('$.coverage', 'coverage is required and must be an object');
87
+ if (!_isArray(record.decisions)) err('$.decisions', 'decisions must be an array');
88
+ if (!_isNonEmptyString(record.generatedAt)) err('$.generatedAt', 'generatedAt is required');
89
+ if (!_isNonEmptyString(record.graphDigest)) err('$.graphDigest', 'graphDigest is required');
90
+
91
+ return { valid: errors.length === 0, errors };
92
+ }
93
+
94
+ function _sensitivityFactor(flow, graph) {
95
+ const de = graph.dataElements.find((d) => flow.dataElementIds.includes(d.id));
96
+ const classes = de?.dataClasses ?? [];
97
+ if (classes.length === 0) return { available: true, tier: 'none', evidence: [] };
98
+ let worst = 'low';
99
+ for (const cls of classes) {
100
+ const sev = DEFAULT_TAXONOMY[cls]?.severity ?? 'medium';
101
+ if ((SEVERITY_RANK[sev] ?? 0) > (SEVERITY_RANK[worst] ?? 0)) worst = sev;
102
+ }
103
+ return { available: true, tier: worst, evidence: [de.id] };
104
+ }
105
+
106
+ function _externalityFactor(flow, nodesById) {
107
+ const sink = nodesById.get(flow.sink);
108
+ const value = sink?.externality?.value ?? 'unknown';
109
+ return { available: true, tier: value, evidence: [flow.sink] };
110
+ }
111
+
112
+ function _controlVerdictFactor(flow) {
113
+ return { available: true, tier: flow.protectionSummary ?? 'not_assessed', evidence: flow.edgeIds ?? [] };
114
+ }
115
+
116
+ function _recipientJurisdictionFactor() {
117
+ // Honest gap — see this file's own header. Never fabricated.
118
+ return { available: false, tier: 'unknown', evidence: [], unavailableReason: 'RecipientProfile extension not yet built (capability #6, Third-Party/Cross-Border Intelligence)' };
119
+ }
120
+
121
+ function _aiUseFactor(flow, nodesById) {
122
+ const sink = nodesById.get(flow.sink);
123
+ const isAi = AI_SINK_SUBTYPES.includes(sink?.subtype);
124
+ return { available: true, tier: isAi ? 'ai_destination' : 'none', evidence: isAi ? [flow.sink] : [] };
125
+ }
126
+
127
+ /**
128
+ * New small aggregation (the scoping doc's own "smallest real addition"
129
+ * for breadth/blast-radius): how many OTHER flows in the same graph
130
+ * share this flow's own sink node or dataElement. A crude but real,
131
+ * non-fabricated proxy for "how widely does this exposure reach" —
132
+ * never presented as a calibrated blast-radius count, just an ordinal
133
+ * tier over a real, disclosed count.
134
+ */
135
+ function _breadthFactor(flow, graph) {
136
+ let sharedCount = 0;
137
+ for (const other of graph.flows) {
138
+ if (other.id === flow.id) continue;
139
+ const sharesSink = other.sink === flow.sink;
140
+ const sharesData = other.dataElementIds.some((id) => flow.dataElementIds.includes(id));
141
+ if (sharesSink || sharesData) sharedCount++;
142
+ }
143
+ const tier = sharedCount >= 5 ? 'high' : sharedCount >= 1 ? 'medium' : 'low';
144
+ return { available: true, tier, evidence: [], sharedFlowCount: sharedCount };
145
+ }
146
+
147
+ function _evidenceConfidenceFactor(flow) {
148
+ const tier = flow.confidence?.tier ?? 'medium';
149
+ return { available: true, tier, evidence: [] };
150
+ }
151
+
152
+ function _policyStateFactor(flow) {
153
+ return { available: true, tier: flow.policyVerdict ?? 'not_evaluated', evidence: flow.evidenceRefs ?? [] };
154
+ }
155
+
156
+ function _changeRecencyFactor() {
157
+ // Honest gap — see this file's own header. Never fabricated.
158
+ return { available: false, tier: 'unknown', evidence: [], unavailableReason: 'GraphSnapshot/GraphDiff not yet built (capability #3, Data-Flow Time Machine)' };
159
+ }
160
+
161
+ /** Score ONE flow on all 9 factors. Never throws on a well-formed graph. */
162
+ export function scoreFlow(flow, graph, nodesById) {
163
+ return {
164
+ flowId: flow.id,
165
+ factors: {
166
+ sensitivity: _sensitivityFactor(flow, graph),
167
+ externality: _externalityFactor(flow, nodesById),
168
+ controlVerdict: _controlVerdictFactor(flow),
169
+ recipientJurisdiction: _recipientJurisdictionFactor(),
170
+ aiUse: _aiUseFactor(flow, nodesById),
171
+ breadth: _breadthFactor(flow, graph),
172
+ evidenceConfidence: _evidenceConfidenceFactor(flow),
173
+ policyState: _policyStateFactor(flow),
174
+ changeRecency: _changeRecencyFactor(),
175
+ },
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Score and rank every flow in the graph. opts.factorOrder (default
181
+ * RANKING_FACTORS) is the PRD's own "transparent configurable factors"
182
+ * requirement, made real: a lexicographic multi-key sort over each
183
+ * factor's own ordinal tier rank, in the given priority order — NEVER a
184
+ * blended single score. Ties within all factors preserve original flow
185
+ * order (stable sort).
186
+ */
187
+ export function rankFlows(graph, opts = {}) {
188
+ const factorOrder = opts.factorOrder ?? RANKING_FACTORS;
189
+ const nodesById = new Map(graph.nodes.map((n) => [n.id, n]));
190
+ const scored = graph.flows.map((flow) => ({
191
+ flow,
192
+ ...scoreFlow(flow, graph, nodesById),
193
+ factorOrderUsed: factorOrder,
194
+ }));
195
+ const rankOf = (scoredFlow, factor) => {
196
+ const f = scoredFlow.factors[factor];
197
+ return _TIER_RANK[factor]?.[f.tier] ?? 0;
198
+ };
199
+ return scored.sort((a, b) => {
200
+ for (const factor of factorOrder) {
201
+ const diff = rankOf(b, factor) - rankOf(a, factor);
202
+ if (diff !== 0) return diff;
203
+ }
204
+ return 0;
205
+ });
206
+ }