@starci/hfs 4.0.7 → 4.0.8

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 (133) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +10 -9
  3. package/bin/hfs.mjs +4 -4
  4. package/lint/run.mjs +1 -1
  5. package/package.json +2 -2
  6. package/runtime/engine/runtime-root.mjs +5 -0
  7. package/runtime/engine/yaml.mjs +3 -3
  8. package/runtime/knowledge/hfs/canon-pins.yaml +10 -10
  9. package/runtime/knowledge/hfs/slots.yaml +5 -5
  10. package/runtime/knowledge/patterns/fe/folder.yaml +2 -2
  11. package/runtime/knowledge/sonar-gate.yaml +2 -2
  12. package/runtime/modules/kernel/failure-codes.yaml +12 -2
  13. package/runtime/scripts/api/fs/lib.mjs +6 -0
  14. package/runtime/scripts/api/fs/rmdir-link.mjs +1 -1
  15. package/runtime/scripts/{lib → api/fs}/safe-remove.mjs +31 -82
  16. package/runtime/scripts/api/git/lib.mjs +1 -1
  17. package/runtime/scripts/api/sops/decrypt.mjs +1 -1
  18. package/runtime/scripts/{lib/hfs-allows.mjs → hfs/allows.mjs} +3 -3
  19. package/runtime/scripts/{checks → hfs}/architecture/backend.mjs +1 -1
  20. package/runtime/scripts/{checks → hfs}/architecture/config.mjs +2 -2
  21. package/runtime/scripts/{checks → hfs}/architecture/connection-map.mjs +1 -1
  22. package/runtime/scripts/{checks → hfs}/architecture/contract-fixture-guard.mjs +1 -1
  23. package/runtime/scripts/{checks → hfs}/architecture/fe-slot-allows.mjs +3 -3
  24. package/runtime/scripts/{checks → hfs}/architecture/feature-shape.mjs +1 -1
  25. package/runtime/scripts/{checks → hfs}/architecture/hfs-graph.mjs +1 -1
  26. package/runtime/scripts/{checks → hfs}/architecture/hfs.mjs +3 -3
  27. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +96 -0
  28. package/runtime/scripts/{checks → hfs}/architecture/next-data.mjs +18 -105
  29. package/runtime/scripts/{checks → hfs}/architecture/required-files.mjs +1 -1
  30. package/runtime/scripts/{checks → hfs}/architecture/surface.mjs +1 -1
  31. package/runtime/scripts/{checks → hfs}/architecture/test-world-files.mjs +2 -2
  32. package/runtime/scripts/{checks → hfs}/architecture/typescript.mjs +1 -1
  33. package/runtime/scripts/{checks → hfs}/architecture.mjs +2 -2
  34. package/runtime/scripts/{lib/hfs-check.mjs → hfs/check.mjs} +33 -25
  35. package/runtime/scripts/hfs/manifest-shape.mjs +170 -0
  36. package/runtime/scripts/{lib/hfs-path-findings.mjs → hfs/path-findings.mjs} +6 -6
  37. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/contract.mjs +3 -2
  38. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +66 -0
  39. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/integration-specs.mjs +1 -1
  40. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/secrets.mjs +2 -2
  41. package/runtime/scripts/{lib/hfs-rules → hfs/rules}/stacks.mjs +2 -2
  42. package/runtime/scripts/{lib/hfs-slots.mjs → hfs/slots.mjs} +52 -58
  43. package/runtime/scripts/{lib/hfs-tree.mjs → hfs/tree.mjs} +1 -1
  44. package/runtime/scripts/{lib/hfs-view.mjs → hfs/view.mjs} +3 -3
  45. package/runtime/scripts/lib/graphql-contract.mjs +429 -0
  46. package/runtime/scripts/lib/is-main.mjs +13 -0
  47. package/runtime/scripts/lib/language.mjs +2 -2
  48. package/runtime/scripts/lib/path-key.mjs +2 -0
  49. package/runtime/scripts/lib/stack-declaration.mjs +2 -2
  50. package/runtime/scripts/{checks/common.mjs → lib/walk.mjs} +1 -12
  51. package/scaffold/app.mjs +1 -1
  52. package/scaffold/service.mjs +2 -2
  53. package/sync/hygiene.mjs +8 -8
  54. package/sync/index.mjs +48 -11
  55. package/templates/app/quality-config/codecov.yml +1 -1
  56. package/templates/app/quality-config/sonar-project.properties +1 -1
  57. package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +2 -2
  58. package/templates/fe/skeleton/apps/__app__/next.config.ts +10 -2
  59. package/templates/fe/skeleton/apps/__app__/src/app/[locale]/error.tsx +2 -2
  60. package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +0 -2
  61. package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +1 -7
  62. package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +0 -2
  63. package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +0 -1
  64. package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +0 -4
  65. package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +1 -1
  66. package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +0 -4
  67. package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +1 -1
  68. package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +0 -4
  69. package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +1 -1
  70. package/templates/fe/skeleton/apps/__app__/src/modules/i18n/request.ts +8 -3
  71. package/templates/fe/skeleton/apps/__app__/src/proxy.ts +1 -1
  72. package/runtime/engine/admission.mjs +0 -284
  73. package/runtime/engine/digest.mjs +0 -10
  74. package/runtime/engine/ledger-db.mjs +0 -1245
  75. package/runtime/engine/machine-db.mjs +0 -1565
  76. package/runtime/engine/migrations/machine/0001-init.sql +0 -887
  77. package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +0 -13
  78. package/runtime/engine/migrations/machine/0003-worktrees-workflow-orca.sql +0 -21
  79. package/runtime/engine/migrations/runtime/0001-init.sql +0 -1072
  80. package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +0 -13
  81. package/runtime/engine/migrations/runtime/0004-attempt-why.sql +0 -43
  82. package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +0 -93
  83. package/runtime/scripts/lib/artifact-hold.mjs +0 -89
  84. package/runtime/scripts/lib/artifact-store.mjs +0 -103
  85. package/runtime/scripts/lib/redact.mjs +0 -148
  86. /package/runtime/scripts/{checks → hfs}/architecture/background-unowned.mjs +0 -0
  87. /package/runtime/scripts/{checks → hfs}/architecture/client-reaches-server.mjs +0 -0
  88. /package/runtime/scripts/{checks → hfs}/architecture/clones.mjs +0 -0
  89. /package/runtime/scripts/{checks → hfs}/architecture/config-unread.mjs +0 -0
  90. /package/runtime/scripts/{checks → hfs}/architecture/constructor-deps.mjs +0 -0
  91. /package/runtime/scripts/{checks → hfs}/architecture/contracts.mjs +0 -0
  92. /package/runtime/scripts/{checks → hfs}/architecture/cross-app-duplicate.mjs +0 -0
  93. /package/runtime/scripts/{checks → hfs}/architecture/dead-exports.mjs +0 -0
  94. /package/runtime/scripts/{checks → hfs}/architecture/default-deny.mjs +0 -0
  95. /package/runtime/scripts/{checks → hfs}/architecture/doc-language.mjs +0 -0
  96. /package/runtime/scripts/{checks → hfs}/architecture/entrypoint.mjs +0 -0
  97. /package/runtime/scripts/{checks → hfs}/architecture/error-codes.mjs +0 -0
  98. /package/runtime/scripts/{checks → hfs}/architecture/error-masked.mjs +0 -0
  99. /package/runtime/scripts/{checks → hfs}/architecture/framework-pinned.mjs +0 -0
  100. /package/runtime/scripts/{checks → hfs}/architecture/frontend.mjs +0 -0
  101. /package/runtime/scripts/{checks → hfs}/architecture/hooks-are-hooks.mjs +0 -0
  102. /package/runtime/scripts/{checks → hfs}/architecture/i18n-keys.mjs +0 -0
  103. /package/runtime/scripts/{checks → hfs}/architecture/index.mjs +0 -0
  104. /package/runtime/scripts/{checks → hfs}/architecture/injection-token-exported.mjs +0 -0
  105. /package/runtime/scripts/{checks → hfs}/architecture/machine-ast.mjs +0 -0
  106. /package/runtime/scripts/{checks → hfs}/architecture/module-per-transport.mjs +0 -0
  107. /package/runtime/scripts/{checks → hfs}/architecture/owners.mjs +0 -0
  108. /package/runtime/scripts/{checks → hfs}/architecture/package-shape.mjs +0 -0
  109. /package/runtime/scripts/{checks → hfs}/architecture/reachability.mjs +0 -0
  110. /package/runtime/scripts/{checks → hfs}/architecture/register-once.mjs +0 -0
  111. /package/runtime/scripts/{checks → hfs}/architecture/registration.mjs +0 -0
  112. /package/runtime/scripts/{checks → hfs}/architecture/route-files-thin.mjs +0 -0
  113. /package/runtime/scripts/{checks → hfs}/architecture/schema-owner.mjs +0 -0
  114. /package/runtime/scripts/{checks → hfs}/architecture/source-names.mjs +0 -0
  115. /package/runtime/scripts/{checks → hfs}/architecture/sql-owner.mjs +0 -0
  116. /package/runtime/scripts/{checks → hfs}/architecture/sql-tokens.mjs +0 -0
  117. /package/runtime/scripts/{checks → hfs}/architecture/symbols.mjs +0 -0
  118. /package/runtime/scripts/{checks → hfs}/architecture/tiers.mjs +0 -0
  119. /package/runtime/scripts/{checks → hfs}/architecture/transport-owner.mjs +0 -0
  120. /package/runtime/scripts/{checks → hfs}/architecture/unit-spec-providers.mjs +0 -0
  121. /package/runtime/scripts/{lib → hfs}/repo-identity.mjs +0 -0
  122. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/deps.mjs +0 -0
  123. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/fe-no-tests.mjs +0 -0
  124. /package/runtime/scripts/{lib/hfs-rules/frontend.mjs → hfs/rules/frontend-tree.mjs} +0 -0
  125. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/lint-suppression.mjs +0 -0
  126. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/peer-integrations.mjs +0 -0
  127. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/pipeline.mjs +0 -0
  128. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/proof-commands.mjs +0 -0
  129. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/read.mjs +0 -0
  130. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/repo-local-checks.mjs +0 -0
  131. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/spec-placement.mjs +0 -0
  132. /package/runtime/scripts/{lib/hfs-rules → hfs/rules}/test-topology.mjs +0 -0
  133. /package/runtime/scripts/{checks → hfs}/typescript-programs.mjs +0 -0
@@ -1,13 +0,0 @@
1
- -- 0003-usage-unavailable (starci/runtime@1, user_version 1 -> 3). Applied by engine/ledger-db.mjs migrateLedger on the first
2
- -- writer open of an older ledger, after a VACUUM INTO backup and an integrity_check, with foreign_key_check + quick_check
3
- -- before COMMIT; a fresh ledger runs 0001 then this file.
4
- --
5
- -- op_attempts.usage_source admits 'unavailable' (the attempt's agent has no usage adapter, or no session file was ever found),
6
- -- and usage_reason says why. SQLite cannot ALTER a CHECK, so the table SQL in sqlite_master is rewritten under
7
- -- writable_schema (the documented path for a change that leaves the on-disk row format alone: the constraint only widens),
8
- -- then schema_version is bumped by the migrator. Every existing row already satisfies the wider constraint.
9
- ALTER TABLE op_attempts ADD COLUMN usage_reason TEXT;
10
- PRAGMA writable_schema=ON;
11
- UPDATE sqlite_master
12
- SET sql=replace(sql,'usage_source IN (''cli-transcript'',''provider-report'')','usage_source IN (''cli-transcript'',''provider-report'',''unavailable'')')
13
- WHERE type='table' AND name='op_attempts' AND instr(sql,'''unavailable''')=0;
@@ -1,43 +0,0 @@
1
- -- 0004-attempt-why (starci/runtime@1, user_version 3 -> 4). Applied by engine/ledger-db.mjs migrateLedger on the first
2
- -- writer open of an older ledger (after a VACUUM INTO backup and an integrity_check, with foreign_key_check + quick_check
3
- -- before COMMIT); a fresh ledger runs 0001 then the forward files.
4
- --
5
- -- op_attempts.why_json: the owner-facing reason an attempt failed, was blocked, refused or is waiting (starci/why@1,
6
- -- scripts/kernel/why.mjs, docs/why.md), written with the settle. The two attempt views are recreated:
7
- -- v_attempt_state.native 'awaiting-owner' (job awaiting_owner, or verdict blocked + report_outcome ask) and
8
- -- 'rejected' (end_state requeued whose settle_json.reason is dispatch-rejected) are their own states;
9
- -- v_op_history carries why_json, and its `ui` reads 'awaiting-owner' and 'rejected' for those two natives
10
- -- (every other native still maps through ui_state_map; job_status stays a column).
11
- ALTER TABLE op_attempts ADD COLUMN why_json TEXT CHECK(why_json IS NULL OR json_valid(why_json));
12
- DROP VIEW IF EXISTS v_op_history;
13
- DROP VIEW IF EXISTS v_attempt_state;
14
- CREATE VIEW v_attempt_state AS
15
- SELECT a.attempt_id,
16
- CASE WHEN a.end_state='worker-dead' THEN 'worker-dead' WHEN a.end_state='effect-unknown' THEN 'effect-unknown'
17
- WHEN a.end_state='requeued' AND json_extract(a.settle_json,'$.reason')='dispatch-rejected' THEN 'rejected'
18
- WHEN a.end_state='requeued' THEN 'requeued'
19
- WHEN (a.verdict='blocked' AND a.report_outcome='ask')
20
- OR (a.verdict IS NOT NULL AND (SELECT j.status FROM jobs j WHERE j.job_id=a.job_id)='awaiting_owner') THEN 'awaiting-owner'
21
- WHEN a.verdict IS NOT NULL THEN a.verdict
22
- WHEN a.dispatched_at IS NULL THEN 'routed' ELSE 'in-flight' END AS native
23
- FROM op_attempts a;
24
- CREATE VIEW v_op_history AS
25
- SELECT a.attempt_id, a.workflow_id, a.unit_id, a.job_id, a.op_id, a.try_no, a.dispatch_seq, a.dispatch_id, a.span_id,
26
- a.agent, a.provider, a.model, a.model_profile, a.pool, a.effort, a.cli_version, a.runtime_rev,
27
- a.terminal_handle, a.worktree_path, a.branch, a.base_sha, a.head_sha, a.integrated_sha,
28
- a.routed_at, a.dispatched_at, a.reported_at, a.settled_at, a.released_at, a.worktree_removed_at,
29
- COALESCE(a.wall_ms, a.settled_at - a.dispatched_at) AS cycle_ms,
30
- a.report_outcome, a.verdict, a.settled_by, a.failure_class, a.end_state, a.why_json,
31
- a.tokens_in, a.tokens_out, a.cost_usd, a.usage_source,
32
- a.prompt_sha, a.transcript_sha, a.session_sha,
33
- s.native AS attempt_state,
34
- CASE WHEN s.native IN ('awaiting-owner','rejected') THEN s.native ELSE COALESCE(m.ui,'unknown') END AS ui,
35
- j.status AS job_status, r.report_id, r.summary AS report_summary,
36
- (SELECT count(*) FROM check_runs c WHERE c.attempt_id=a.attempt_id) AS checks,
37
- (SELECT count(*) FROM check_runs c WHERE c.attempt_id=a.attempt_id AND c.status IN ('fail','error')) AS checks_red,
38
- (SELECT count(*) FROM job_artifacts x WHERE x.attempt_id=a.attempt_id) AS artifacts
39
- FROM op_attempts a
40
- JOIN v_attempt_state s ON s.attempt_id=a.attempt_id
41
- LEFT JOIN ui_state_map m ON m.entity='attempt' AND m.native=s.native
42
- LEFT JOIN jobs j ON j.job_id=a.job_id
43
- LEFT JOIN reports r ON r.attempt_id=a.attempt_id;
@@ -1,93 +0,0 @@
1
- -- 0005-ended-workflow-views (starci/runtime@1, user_version 4 -> 5). Applied by engine/ledger-db.mjs migrateLedger on the first
2
- -- writer open of an older ledger (after a VACUUM INTO backup and an integrity_check, with foreign_key_check + quick_check
3
- -- before COMMIT); a fresh ledger runs 0001 then the forward files.
4
- --
5
- -- The runtime views stop surfacing the live leftovers of an ENDED workflow (phase archived|finished): archive/finish close
6
- -- the workflow's live Decision Items inside their own transaction before the phase flips, and events_refuse_archived refuses
7
- -- every later write, so a leftover live DI (2026-09-30 nivo-backend di-876124d7) can never be resolved and only ever reads
8
- -- 'bad' in the UI — a permanent attention item and blocker on work that no longer runs. Every leg carries the ENDED
9
- -- predicate of scripts/reconciler/decisions.mjs listDecisions: LEFT JOIN workflows w; the row survives when its workflow is
10
- -- missing or not ended (w.phase IS NULL OR w.phase NOT IN ('archived','finished')).
11
- -- v_decision_rows keeps an ended workflow's resolved rows (decision history) and drops only its live-status leftovers;
12
- -- v_blocking applies the join per UNION leg — an ended workflow owes no unit, decision, condition, incident,
13
- -- worker-question, settle-tail, product-land or settle-overdue row;
14
- -- v_open_work the same per leg (decision + job).
15
- DROP VIEW IF EXISTS v_decision_rows;
16
- DROP VIEW IF EXISTS v_blocking;
17
- DROP VIEW IF EXISTS v_open_work;
18
- CREATE VIEW v_decision_rows AS
19
- SELECT d.*,
20
- (d.due_at IS NOT NULL AND d.status IN ('open','claimed','escalated') AND d.due_at < CAST(unixepoch('subsec')*1000 AS INTEGER)) AS overdue,
21
- CASE WHEN d.status IN ('open','claimed','escalated')
22
- AND ((d.due_at IS NOT NULL AND d.due_at < CAST(unixepoch('subsec')*1000 AS INTEGER)) OR d.escalations>=2) THEN 'bad'
23
- WHEN d.status='open' AND d.due_at IS NOT NULL
24
- AND (d.due_at - CAST(unixepoch('subsec')*1000 AS INTEGER)) < 0.2*(d.due_at - d.opened_at) THEN 'warn'
25
- ELSE COALESCE(m.ui,'unknown') END AS ui
26
- FROM decision_items d LEFT JOIN ui_state_map m ON m.entity='decision' AND m.native=d.status
27
- LEFT JOIN workflows w ON w.workflow_id=d.workflow_id
28
- WHERE d.status NOT IN ('open','claimed','escalated') OR w.phase IS NULL OR w.phase NOT IN ('archived','finished');
29
-
30
- CREATE VIEW v_blocking AS
31
- SELECT e.workflow_id, 'unit' AS entity_type, e.to_unit AS entity_id, 'unit' AS blocker_type, e.from_unit AS blocker_id,
32
- 'UpstreamNotDone' AS reason_code, p.state AS detail, p.updated_at AS since, 'kernel' AS who
33
- FROM unit_edges e JOIN work_units t ON t.workflow_id=e.workflow_id AND t.unit_id=e.to_unit
34
- JOIN work_units p ON p.workflow_id=e.workflow_id AND p.unit_id=e.from_unit
35
- LEFT JOIN workflows w ON w.workflow_id=e.workflow_id
36
- WHERE t.state IN ('planned','queued') AND p.state NOT IN ('done','dropped')
37
- AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
38
- UNION ALL
39
- SELECT d.workflow_id, COALESCE(d.entity_type,'workflow'), COALESCE(d.entity_id,d.workflow_id), 'decision', d.di_id,
40
- 'DecisionOpen:'||d.kind, d.summary, d.opened_at, d.decider
41
- FROM decision_items d LEFT JOIN workflows w ON w.workflow_id=d.workflow_id
42
- WHERE d.status IN ('open','claimed','escalated')
43
- AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
44
- UNION ALL
45
- SELECT c.workflow_id, c.entity_type, c.entity_id, 'condition', c.type, c.reason, c.message, c.last_transition_at, COALESCE(c.owner,'controller')
46
- FROM conditions c LEFT JOIN workflows w ON w.workflow_id=c.workflow_id
47
- WHERE c.status<>'True'
48
- AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
49
- UNION ALL
50
- SELECT i.workflow_id, 'job', COALESCE(i.job_id,i.workflow_id), 'incident', i.incident_id, 'IncidentOpen:'||i.kind,
51
- COALESCE(i.detail,i.last_progress), i.created_at, i.owner
52
- FROM incidents i LEFT JOIN workflows w ON w.workflow_id=i.workflow_id
53
- WHERE i.status='open'
54
- AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
55
- UNION ALL
56
- SELECT x.workflow_id, 'attempt', CAST(x.attempt_id AS TEXT), 'question', CAST(x.inbox_id AS TEXT), 'WorkerQuestionPending',
57
- json_extract(x.payload_json,'$.question'), x.created_at, 'kernel'
58
- FROM inbox x LEFT JOIN workflows w ON w.workflow_id=x.workflow_id
59
- WHERE x.kind='worker-question' AND x.status IN ('pending','claimed')
60
- AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
61
- UNION ALL
62
- SELECT s.workflow_id, 'attempt', CAST(s.attempt_id AS TEXT), 'settle-tail', CAST(s.attempt_id AS TEXT), 'SettleTailFailed', s.last_error,
63
- COALESCE(s.started_at,s.queued_at), 'controller'
64
- FROM settle_tails s LEFT JOIN workflows w ON w.workflow_id=s.workflow_id
65
- WHERE s.state='failed'
66
- AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
67
- UNION ALL
68
- SELECT l.workflow_id, 'workflow', l.workflow_id, 'product-land', CAST(l.land_id AS TEXT), 'ProductLand:'||l.result, l.reason,
69
- l.started_at, 'kernel'
70
- FROM product_lands l LEFT JOIN workflows w ON w.workflow_id=l.workflow_id
71
- WHERE l.result IN ('failed','conflict','red')
72
- AND NOT EXISTS(SELECT 1 FROM product_lands k WHERE k.workflow_id=l.workflow_id AND k.land_id>l.land_id AND k.result='landed')
73
- AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
74
- UNION ALL
75
- -- H1: a filed report nobody settled within its SLA (the same rows as v_settle_overdue) blocks its job.
76
- SELECT a.workflow_id, 'attempt', CAST(a.attempt_id AS TEXT), 'settle', CAST(a.attempt_id AS TEXT),
77
- 'SettleOverdue:'||COALESCE(a.report_outcome,'?'), a.job_id, a.reported_at,
78
- CASE WHEN a.report_outcome='done' THEN 'settler' ELSE 'kernel' END
79
- FROM op_attempts a LEFT JOIN workflows w ON w.workflow_id=a.workflow_id
80
- WHERE a.reported_at IS NOT NULL AND a.settled_at IS NULL AND a.end_state IS NULL
81
- AND (CAST(unixepoch('subsec')*1000 AS INTEGER) - a.reported_at) > CASE WHEN a.report_outcome='done' THEN 180000 ELSE 900000 END
82
- AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'));
83
-
84
- CREATE VIEW v_open_work AS
85
- SELECT 'decision' AS kind, d.workflow_id, d.di_id AS ref, d.summary AS what, d.opened_at AS since, d.due_at
86
- FROM decision_items d LEFT JOIN workflows w ON w.workflow_id=d.workflow_id
87
- WHERE d.status IN ('open','claimed','escalated')
88
- AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'))
89
- UNION ALL
90
- SELECT 'job-'||j.status, j.workflow_id, j.job_id, j.op_id, j.updated_at, j.deadline
91
- FROM jobs j LEFT JOIN workflows w ON w.workflow_id=j.workflow_id
92
- WHERE j.status IN ('leased','running','answering','effect_unknown')
93
- AND (w.phase IS NULL OR w.phase NOT IN ('archived','finished'));
@@ -1,89 +0,0 @@
1
- // artifact-hold.mjs — the retention exemption for job proofs. A path an indexed job artifact lives at
2
- // (job_artifacts, engine/migrations/runtime/0001-init.sql), a tree holding one, and the evidence directory around one (never a shared root:
3
- // the repository, .starciwork, .starciwork/evidence or .starciwork/kernel-evidence itself) are never
4
- // removed by housekeeping, whatever the workflow's phase (running, finished, archived); nor is the repository's
5
- // ledger .starciwork/runtime.sqlite (it holds the typed logs since 2026-09-27, scripts/kernel/typed-logs.mjs), nor the
6
- // retired logs file .starciwork/logs.sqlite or its logs.sqlite.migrated-<date> copy, nor a tree holding one. Every
7
- // runtime delete of a tree (safe-remove.mjs safeRemoveTree) and every housekeeping unlink asks artifactHoldOf first.
8
- // Proofs, artifacts and logs are deleted ONLY by the owner-approved workflow purge (scripts/work/purge-workflow.mjs:
9
- // archive to a verified ZIP, then delete the finished workflow as a unit), never here.
10
- //
11
- // The ledgers are the ones the machine registry enrols (machine.sqlite `ledgers`); a ledger's repository is
12
- // the directory above its .starciwork. Only a target that overlaps a registered repository opens that
13
- // ledger, read-only, so a temp or lane sweep never touches sqlite past the registry read.
14
- import fs from 'node:fs';
15
- import path from 'node:path';
16
- import { createRequire } from 'node:module';
17
- import { hasLedgerColumn, hasLedgerTable, openLedgerReader } from '../../engine/ledger-db.mjs';
18
- import { machineFileFor, readMachine } from '../../engine/machine-db.mjs';
19
- import { pathKey, sameOrUnder } from './path-key.mjs';
20
-
21
- const require = createRequire(import.meta.url);
22
- const REGISTRY_TTL_MS = 30000;
23
- const norm = pathKey;
24
- let cache = { file: null, at: 0, repos: [] };
25
-
26
- const openReadOnly = (file) => openLedgerReader(file);
27
-
28
- /** The repositories whose ledgers the registry enrols: [{repo, ledger}], cached briefly per registry file; null when the registry cannot be read. */
29
- export function registeredRepos({ env = process.env, now = Date.now() } = {}) {
30
- const file = machineFileFor(env);
31
- if (cache.file === file && now - cache.at < REGISTRY_TTL_MS) return cache.repos;
32
- const repos = fs.existsSync(file) ? readMachine((m) => m.listLedgers().map((l) => ({ ledger: l.file, repo: l.repoRoot })), null, { file, env }) : [];
33
- cache = { file, at: now, repos };
34
- return repos;
35
- }
36
-
37
- /**
38
- * Null when removing `target` touches no indexed artifact; else {ledger, repo, paths:[...up to 5], count}.
39
- * Held: an indexed file at or sameOrUnder `target`, or `target` inside the directory of an indexed file.
40
- * A ledger that cannot be read holds its whole repository, and an unreadable registry holds every path in or
41
- * holding a .starciwork directory - where every indexed artifact lives (fail closed).
42
- */
43
- export function artifactHoldOf(target, { env = process.env, repos = registeredRepos({ env }) } = {}) {
44
- const t = norm(target);
45
- if (repos === null) {
46
- const work = t.split('/').includes('.starciwork') || fs.existsSync(path.join(target, '.starciwork'));
47
- return work ? { ledger: machineFileFor(env), repo: null, paths: [], count: null, error: 'the machine registry cannot be read' } : null;
48
- }
49
- for (const { ledger, repo } of repos) {
50
- const r = norm(repo);
51
- if (!sameOrUnder(t, r) && !sameOrUnder(r, t)) continue;
52
- // The repository's typed logs (scripts/kernel/typed-logs.mjs: the ledger's logs table, and the retired logs.sqlite
53
- // with its migrated copy) are append-only history: never swept.
54
- const work = path.join(repo, '.starciwork');
55
- const logFiles = [path.basename(ledger), 'logs.sqlite', ...(fs.existsSync(work) && sameOrUnder(norm(work), t) ? fs.readdirSync(work).filter((n) => /^logs\.sqlite\.migrated-/.test(n)) : [])]
56
- .filter((name, i, all) => all.indexOf(name) === i).map((name) => path.join(work, name)).filter((file) => sameOrUnder(norm(file), t) && fs.existsSync(file));
57
- if (logFiles.length) return { ledger, repo, paths: logFiles.map((file) => `.starciwork/${path.basename(file)}`), count: logFiles.length };
58
- if (!fs.existsSync(ledger)) continue;
59
- let paths;
60
- try {
61
- const db = openReadOnly(ledger);
62
- // alpha.3: job_artifacts index blobs by sha256 and hold no repository path (no `path` column), so an alpha.3
63
- // ledger holds no tree; only a ledger that still indexes paths does.
64
- try { paths = hasLedgerTable(db, 'job_artifacts') && hasLedgerColumn(db, 'job_artifacts', 'path') ? db.prepare('SELECT DISTINCT path FROM job_artifacts').all().map((row) => row.path) : []; }
65
- finally { db.close(); }
66
- } catch (error) {
67
- return { ledger, repo, paths: [], count: null, error: String(error?.message ?? error) };
68
- }
69
- // "Around" holds an evidence directory, never a shared root: an artifact filed at the repository root or directly
70
- // in .starciwork (e.g. a scope op indexing .starciwork/index.yaml) held every tree sameOrUnder it, so no purge could
71
- // remove an archived workflow's kernel-evidence directory.
72
- const shared = new Set([r, norm(work), norm(path.join(work, 'evidence')), norm(path.join(work, 'kernel-evidence'))]);
73
- const held = paths.filter((rel) => {
74
- const abs = norm(path.join(repo, rel));
75
- const dir = norm(path.dirname(abs));
76
- return sameOrUnder(abs, t) || (!shared.has(dir) && sameOrUnder(t, dir));
77
- });
78
- if (held.length) return { ledger, repo, paths: held.slice(0, 5), count: held.length };
79
- }
80
- return null;
81
- }
82
-
83
- /** artifactHoldOf as one refusal line, or null. */
84
- export const artifactHoldReason = (target, options) => {
85
- const hold = artifactHoldOf(target, options);
86
- if (!hold) return null;
87
- return hold.error ? `the ledger ${hold.ledger} holding indexed job artifacts cannot be read (${hold.error})`
88
- : `${hold.count} indexed job artifact(s) of ${hold.ledger} (${hold.paths.join(', ')})`;
89
- };
@@ -1,103 +0,0 @@
1
- // Content-addressed operational evidence. Blob bytes are immutable and never removed here.
2
- import fs from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import crypto from 'node:crypto';
6
-
7
- export const ARTIFACT_ROOT_ENV = 'STARCI_ARTIFACT_ROOT';
8
- export const artifactRoot = (env = process.env) => path.resolve(env[ARTIFACT_ROOT_ENV] || path.join(os.homedir(), '.starci', 'artifacts'));
9
- const SHA = /^[a-f0-9]{64}$/;
10
- const assertSha = sha => {
11
- if (typeof sha !== 'string' || !SHA.test(sha)) throw new TypeError('blob sha must be a lowercase sha256 hex digest');
12
- return sha;
13
- };
14
- const location = sha => path.join(artifactRoot(), assertSha(sha).slice(0, 2), sha);
15
- const metadataPath = sha => `${location(sha)}.json`;
16
- const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
17
-
18
- // Reject roots inside a checkout, including worktrees whose .git is a file.
19
- function ensureExternalRoot(root) {
20
- let cursor = root;
21
- while (true) {
22
- if (fs.existsSync(path.join(cursor, '.git'))) throw new Error(`artifact root is inside a git checkout: ${root}`);
23
- const parent = path.dirname(cursor);
24
- if (parent === cursor) return;
25
- cursor = parent;
26
- }
27
- }
28
-
29
- function sourceBytes(bufferOrPath) {
30
- if (Buffer.isBuffer(bufferOrPath)) return bufferOrPath;
31
- if (bufferOrPath instanceof Uint8Array) return Buffer.from(bufferOrPath);
32
- if (typeof bufferOrPath === 'string') return fs.readFileSync(bufferOrPath);
33
- throw new TypeError('putBlob expects a Buffer, Uint8Array, or file path');
34
- }
35
-
36
- function publishOnce(destination, bytes) {
37
- const dir = path.dirname(destination);
38
- fs.mkdirSync(dir, { recursive: true });
39
- const temp = path.join(dir, `.${path.basename(destination)}.${process.pid}.${crypto.randomUUID()}.tmp`);
40
- try {
41
- fs.writeFileSync(temp, bytes, { flag: 'wx' });
42
- try { fs.linkSync(temp, destination); }
43
- catch (error) { if (error.code !== 'EEXIST') throw error; }
44
- } finally { try { fs.unlinkSync(temp); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
45
- }
46
-
47
- /** Store original bytes under their sha256. A concurrent or repeated put never replaces published bytes. */
48
- export function putBlob(bufferOrPath, { mediaType = 'application/octet-stream' } = {}) {
49
- if (typeof mediaType !== 'string' || !mediaType.trim()) throw new TypeError('mediaType must be a nonempty string');
50
- const root = artifactRoot();
51
- ensureExternalRoot(root);
52
- const bytes = sourceBytes(bufferOrPath);
53
- const sha = digest(bytes);
54
- const destination = location(sha);
55
- const metadata = { size: bytes.length, mediaType, createdAt: new Date().toISOString() };
56
- // Metadata is published first so a visible blob always has a sidecar. A crash can leave an orphan sidecar;
57
- // the next put completes it. The first published media type and creation time win.
58
- publishOnce(metadataPath(sha), Buffer.from(JSON.stringify(metadata)));
59
- publishOnce(destination, bytes);
60
- const stored = fs.statSync(destination);
61
- if (stored.size !== bytes.length) throw new Error(`blob size mismatch: ${sha}`);
62
- const info = statBlob(sha);
63
- return { sha, size: info.size, mediaType: info.mediaType };
64
- }
65
-
66
- export function blobPath(sha) {
67
- const file = location(sha);
68
- return fs.existsSync(file) && fs.statSync(file).isFile() ? file : null;
69
- }
70
-
71
- export const hasBlob = sha => blobPath(sha) !== null;
72
-
73
- export function getBlob(sha) {
74
- const file = blobPath(sha);
75
- if (!file) throw Object.assign(new Error(`blob not found: ${sha}`), { code: 'ENOENT' });
76
- const bytes = fs.readFileSync(file);
77
- if (digest(bytes) !== sha) throw new Error(`blob hash mismatch: ${sha}`);
78
- return bytes;
79
- }
80
-
81
- export function statBlob(sha) {
82
- const file = blobPath(sha);
83
- if (!file) return null;
84
- const { size } = fs.statSync(file);
85
- const metadata = JSON.parse(fs.readFileSync(metadataPath(sha), 'utf8'));
86
- if (metadata.size !== size) throw new Error(`blob sidecar size mismatch: ${sha}`);
87
- return { sha, size, mediaType: metadata.mediaType, createdAt: metadata.createdAt };
88
- }
89
-
90
- function canonical(value) {
91
- if (value && typeof value === 'object') {
92
- if (typeof value.toJSON === 'function') return canonical(value.toJSON());
93
- if (Array.isArray(value)) return value.map(item => item === undefined ? null : canonical(item));
94
- return Object.fromEntries(Object.keys(value).sort().filter(key => value[key] !== undefined).map(key => [key, canonical(value[key])]));
95
- }
96
- return value;
97
- }
98
-
99
- export function putJson(obj) {
100
- const encoded = JSON.stringify(canonical(obj));
101
- if (encoded === undefined) throw new TypeError('putJson expects a JSON value');
102
- return putBlob(Buffer.from(encoded), { mediaType: 'application/json' }).sha;
103
- }
@@ -1,148 +0,0 @@
1
- // redact.mjs — the ONE redaction module (alpha.3, ARCHITECTURE-DB §4.2). Every blob put of text bytes
2
- // (report attachments, check stdout/stderr/output, terminal transcripts, CLI sessions) and every log write
3
- // passes through here first; the harness UI server reuses it. A blob whose bytes went through redactText is
4
- // stamped blobs.redaction = REDACTION_VERSION; bytes that cannot be filtered (images, video, archives) are
5
- // stamped 'binary'.
6
- //
7
- // It blanks: provider tokens and JWTs (secret-patterns.mjs, the push scan's list), auth headers, URL query
8
- // secrets, URL credentials (scheme://user:pass@host), PEM private-key blocks (the whole block), KEY=value /
9
- // KEY: value pairs whose key names a secret (FOO_API_KEY=..., password: ...), and the declared secrets of a
10
- // product repo's .starcistacks (their env-style names as keys, and their values wherever they appear).
11
- // Keys and surrounding words stay, so a redacted transcript still reads.
12
- import fs from 'node:fs';
13
- import path from 'node:path';
14
- import { FORBIDDEN_FILES, SECRET_PATTERNS } from './secret-patterns.mjs';
15
- import { parseYaml } from '../../engine/yaml.mjs';
16
-
17
- export const REDACTION_VERSION = 'v1';
18
- export const MARK = '[redacted]';
19
-
20
- const withGlobal = (re) => new RegExp(re.source, re.flags.includes('g') ? re.flags : `${re.flags}g`);
21
- const PATTERNS = SECRET_PATTERNS.filter((rule) => rule.name !== 'private-key-block').map((rule) => ({ ...rule, g: withGlobal(rule.re) }));
22
- // Words that make an env-style key a secret, matched per `_`-separated segment of the name (FOO_API_KEY,
23
- // KEYCLOAK_ADMIN_PASSWORD, GH_TOKEN, MINIO_SECRET_KEY, DATABASE_URL is not; TESTS_PASSED is not).
24
- const SECRET_SEGMENTS = new Set(['PASSWORD', 'PASSWD', 'PWD', 'PASS', 'SECRET', 'SECRETS', 'TOKEN', 'APIKEY', 'KEY', 'CREDENTIAL', 'CREDENTIALS', 'COOKIE', 'OTP', 'DSN', 'PAT']);
25
- export const isSecretEnvName = (name) => String(name).replace(/^export\s+/, '').replace(/_FILE$/, '').split('_').some((seg) => SECRET_SEGMENTS.has(seg));
26
- const PEM_BLOCK = /-----BEGIN ((?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?)-----[\s\S]*?(?:-----END \1-----|$)/g;
27
- const RULES = [
28
- { name: 'url-credentials', re: /\b([a-z][a-z0-9+.-]*:\/\/)([^\s:@/"'<>]+):([^\s@/"'<>]+)@/gi, to: (m, scheme, user) => `${scheme}${user}:${MARK}@` },
29
- { name: 'auth-header', re: /\b(Bearer|Basic|Token)(\s+)[A-Za-z0-9._~+/=-]{8,}/gi, to: (m, a, b) => `${a}${b}${MARK}` },
30
- { name: 'url-secret', re: /([?&#](?:access_token|refresh_token|id_token|token|key|api_key|apikey|secret|code|password|otp|sig|signature)=)([^&#\s"']+)/gi, to: (m, a) => `${a}${MARK}` },
31
- { name: 'otp', re: /\b(otp|one[-_ ]?time[-_ ]?(?:code|password|pin)|verification[-_ ]?code|2fa[-_ ]?code|mã[ _-]?(?:otp|xác[ _-]?(?:thực|minh)))(\s*[:=]?\s*["']?)(\d{4,8})\b/giu, to: (m, a, b) => `${a}${b}${MARK}` },
32
- // ENV_STYLE_KEY=value / ENV_STYLE_KEY: value (shell exports, dotenv, compose, CLI echo).
33
- { name: 'env-secret', re: /\b((?:export\s+)?[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*)(\s*[:=]\s*["']?)((?!\[redacted)[^\s"'`]{3,})/g,
34
- to: (m, a, b, c) => (isSecretEnvName(a) && !c.startsWith('/run/secrets/') && !c.startsWith('$') ? `${a}${b}${MARK}` : m) },
35
- { name: 'keyed-secret', re: /\b(password|passwd|pwd|secret|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|auth[_-]?token|session[_-]?token|private[_-]?key|otp|pin[_-]?code|cookie|set-cookie)(["']?\s*[:=]\s*["']?)((?!\[redacted)[^\s"',;&}]{3,})/gi, to: (m, a, b) => `${a}${b}${MARK}` },
36
- ];
37
-
38
- /** Keys whose value is a secret whatever it looks like; the key stays, the value goes. */
39
- export const SECRET_KEY = /^(?:password|passwd|pwd|pass|secret|otp|pin|pincode|pin_code|credential|credentials|authorization|cookie|cookies|set-cookie|private[_-]?key|client[_-]?secret|api[_-]?key|apikey|[a-z_-]*token|[a-z_-]*secret)$/i;
40
-
41
- // ------------------------------------------------------------------------------ declared stack secrets
42
- // A product repo's .starcistacks/<stack>/stack.yaml `secrets:` block names each secret and the runtime file
43
- // that holds its value. Both are learned once per repo root per process; values never leave this module.
44
- const STACK_DIRS = ['.starcistacks'];
45
- const learnedRoots = new Set();
46
- const secretValues = new Set();
47
- const secretNames = new Set();
48
- const envName = (name) => String(name).replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '').toUpperCase();
49
- const readSafe = (file) => { try { return fs.readFileSync(file, 'utf8'); } catch { return null; } };
50
- const listSafe = (dir) => { try { return fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; } };
51
-
52
- /** A value this process must never write out (a secret it resolved itself). Values under 6 chars are ignored. */
53
- export function addSecretValue(value) {
54
- const v = typeof value === 'string' ? value.trim() : '';
55
- if (v.length >= 6 && !/^\[redacted/.test(v)) secretValues.add(v);
56
- }
57
-
58
- /** Learn the declared secrets of every .starcistacks stack under `repoRoot` (idempotent). */
59
- export function learnStackSecrets(repoRoot) {
60
- if (typeof repoRoot !== 'string' || !repoRoot) return;
61
- const root = path.resolve(repoRoot);
62
- if (learnedRoots.has(root)) return;
63
- learnedRoots.add(root);
64
- for (const stackDir of STACK_DIRS) {
65
- for (const entry of listSafe(path.join(root, stackDir))) {
66
- if (!entry.isDirectory()) continue;
67
- const dir = path.join(root, stackDir, entry.name);
68
- const text = readSafe(path.join(dir, 'stack.yaml'));
69
- let doc = null;
70
- try { doc = text ? parseYaml(text) : null; } catch { doc = null; }
71
- const secrets = doc && typeof doc.secrets === 'object' && doc.secrets ? doc.secrets : {};
72
- for (const [name, spec] of Object.entries(secrets)) {
73
- secretNames.add(envName(name));
74
- if (spec && typeof spec.file === 'string') addSecretValue(readSafe(path.resolve(dir, spec.file)));
75
- }
76
- // Plaintext runtime copies of the secrets (git-ignored): their values are secrets too.
77
- for (const file of listSafe(path.join(dir, 'runtime', 'files'))) if (file.isFile()) addSecretValue(readSafe(path.join(dir, 'runtime', 'files', file.name)));
78
- }
79
- }
80
- }
81
-
82
- const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
83
- let namesRe = null, namesSize = -1;
84
- const declaredNameRule = () => {
85
- if (namesSize !== secretNames.size) {
86
- namesSize = secretNames.size;
87
- namesRe = secretNames.size ? new RegExp(`\\b((?:${[...secretNames].map(escapeRe).join('|')})(?:_FILE)?)(\\s*[:=]\\s*["']?)((?!\\[redacted)[^\\s"'\`]{3,})`, 'g') : null;
88
- }
89
- return namesRe;
90
- };
91
-
92
- // --------------------------------------------------------------------------------------------- text
93
- /**
94
- * `text` with every secret value blanked. `repoRoots` (optional) are product repos whose .starcistacks
95
- * declarations are learned first.
96
- */
97
- export function redactText(text, { repoRoots = [] } = {}) {
98
- if (typeof text !== 'string' || !text) return text;
99
- for (const root of repoRoots) learnStackSecrets(root);
100
- let out = text.replace(PEM_BLOCK, (m, kind) => `[redacted:pem ${kind}]`);
101
- for (const value of secretValues) if (out.includes(value)) out = out.split(value).join('[redacted:stack-secret]');
102
- for (const rule of PATTERNS) {
103
- rule.g.lastIndex = 0;
104
- out = out.replace(rule.g, (match, value) => {
105
- if (rule.name === 'assigned-secret') return rule.placeholder?.test(value ?? '') ? match : match.replace(value, MARK);
106
- return `[redacted:${rule.name}]`;
107
- });
108
- }
109
- const declared = declaredNameRule();
110
- if (declared) { declared.lastIndex = 0; out = out.replace(declared, (m, a, b) => `${a}${b}${MARK}`); }
111
- for (const rule of RULES) { rule.re.lastIndex = 0; out = out.replace(rule.re, rule.to); }
112
- return out;
113
- }
114
-
115
- /** A path that is a secret by being one (an env file, a key file, .secrets/): blanked, its rule named. */
116
- export function redactPath(p) {
117
- if (typeof p !== 'string') return p;
118
- const slashed = p.replace(/\\/g, '/');
119
- const rule = FORBIDDEN_FILES.find((r) => r.test(slashed));
120
- return rule ? `[redacted:${rule.name}]` : redactText(p);
121
- }
122
-
123
- /** A deep copy of `value` with secret-named keys blanked and every string redacted. */
124
- export function redactData(value, key = null, depth = 0) {
125
- if (depth > 8) return '[depth]';
126
- if (key && SECRET_KEY.test(key) && value != null && typeof value !== 'object' && typeof value !== 'boolean') return MARK;
127
- if (typeof value === 'string') return /(?:path|ref|file)$/i.test(key ?? '') ? redactPath(value) : redactText(value);
128
- if (Array.isArray(value)) return value.map((v) => redactData(v, null, depth + 1));
129
- if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactData(v, k, depth + 1)]));
130
- return value;
131
- }
132
-
133
- // -------------------------------------------------------------------------------------------- bytes
134
- /** True for media types whose bytes are text the filter can read. */
135
- export const isTextMedia = (mediaType) => /^text\/|[/+](?:json|xml|yaml|x-yaml|javascript|x-ndjson|x-diff|x-patch|csv|sql|x-sh)\b|^application\/(?:json|xml|yaml|javascript|x-ndjson|x-diff|x-patch|sql)$/i
136
- .test(String(mediaType ?? ''));
137
-
138
- /**
139
- * Bytes ready for a blob put: text bytes redacted ({redaction:'v1'}), anything else untouched
140
- * ({redaction:'binary'}). The blob sha is then taken over the returned bytes.
141
- */
142
- export function redactBytes(bytes, mediaType, { repoRoots = [] } = {}) {
143
- const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
144
- if (!isTextMedia(mediaType)) return { bytes: buf, redaction: 'binary' };
145
- const text = buf.toString('utf8');
146
- const clean = redactText(text, { repoRoots });
147
- return { bytes: clean === text ? buf : Buffer.from(clean, 'utf8'), redaction: REDACTION_VERSION };
148
- }
File without changes