@tequila99/githuman 0.12.0 → 0.13.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/README.md +12 -0
- package/dist/cli/config.js +3 -0
- package/dist/cli/server-runtime.js +4 -0
- package/dist/server/app.js +3 -0
- package/dist/server/hooks/access/local-request.js +5 -0
- package/dist/server/hooks/access/require-access.js +5 -2
- package/dist/server/repositories/artifact-validation.repo.js +11 -0
- package/dist/server/routes/artifact-actions.js +1 -1
- package/dist/server/routes/artifact-agent.js +18 -2
- package/dist/server/services/agent/session-registry.js +31 -9
- package/dist/server/services/artifact-authorization.service.js +26 -1
- package/dist/server/services/artifact-graph-validation.js +48 -4
- package/dist/server/services/artifact-lifecycle.service.js +95 -7
- package/dist/server/services/artifact-storage.service.js +2 -2
- package/dist/server/services/artifact-transition.service.js +2 -1
- package/dist/server/services/artifact-validation-jobs.service.js +361 -31
- package/dist/shared/agents/schemas.js +3 -0
- package/dist/shared/artifact-tools/descriptions.js +6 -0
- package/dist/shared/artifact-tools/schemas.js +6 -1
- package/dist/shared/artifacts/schemas.js +56 -2
- package/dist/web/assets/{AccessPage-Dvj0aCkm.js → AccessPage-D9xfFUBC.js} +1 -1
- package/dist/web/assets/{AddButton-CMC9qf51.js → AddButton-DKNKmS1i.js} +1 -1
- package/dist/web/assets/AddToAgentButton-wC1dzbDE.js +1 -0
- package/dist/web/assets/ArtifactsPage-IHUO7SbH.js +2 -0
- package/dist/web/assets/{ArtifactsPage-Cu1xdYNf.css → ArtifactsPage-nRD9XiRm.css} +1 -1
- package/dist/web/assets/ClosePopup-Bd5a-Lu_.js +1 -0
- package/dist/web/assets/{CreateReviewFabButton-BCg1TvFh.js → CreateReviewFabButton-D58IHr36.js} +1 -1
- package/dist/web/assets/DialogHeader-BVrtb0Ri.js +1 -0
- package/dist/web/assets/IndexPage-BJ1EQdlT.js +1 -0
- package/dist/web/assets/{MainLayout-Dy74m_q1.css → MainLayout-C3Ie5yOo.css} +1 -1
- package/dist/web/assets/MainLayout-CeogGMHH.js +7 -0
- package/dist/web/assets/{NoteVisualEditor-5rHBQ3j0.js → NoteVisualEditor-DqAZZig1.js} +3 -3
- package/dist/web/assets/NotesPage-DXgI-kwN.js +4 -0
- package/dist/web/assets/{PdfPreview-2NQ5gkjh.js → PdfPreview-ByeBjQMQ.js} +1 -1
- package/dist/web/assets/{ProjectFilePage-B-WUUxnq.js → ProjectFilePage-DV1anYfN.js} +1 -1
- package/dist/web/assets/QFab-DJhLqo9i.js +1 -0
- package/dist/web/assets/QScrollArea-qf2G1s2r.js +1 -0
- package/dist/web/assets/QSeparator-B8uLaDiI.js +1 -0
- package/dist/web/assets/{QSpace-C7iAnGlU.js → QSpace-CVp0CPR_.js} +1 -1
- package/dist/web/assets/QSplitter-K2LjbupZ.js +1 -0
- package/dist/web/assets/QTabs-Bl_rIz4_.js +1 -0
- package/dist/web/assets/QVirtualScroll-BeEkuL6x.js +1 -0
- package/dist/web/assets/ReviewDetailPage-CL4Dmz0L.js +1 -0
- package/dist/web/assets/ReviewsPage-BPJFTulG.js +1 -0
- package/dist/web/assets/SaveButton-D1q2suzR.js +3 -0
- package/dist/web/assets/{ArtifactDefinitionPicker-D3qdrxlC.css → SaveButton-Vtjvcbt0.css} +1 -1
- package/dist/web/assets/{SearchInput-BRyN93Z1.js → SearchInput-DapVXrFj.js} +1 -1
- package/dist/web/assets/StatusSwitcher-BtbSIr5q.js +1 -0
- package/dist/web/assets/{TagFilterField-Bl3FBkB5.js → TagFilterField-CPl-BxtL.js} +1 -1
- package/dist/web/assets/{TerminalView-tEyLM5qZ.js → TerminalView-B56cEx8g.js} +1 -1
- package/dist/web/assets/{abnfDiagram-O67JEVCF-DN9xiV9p.js → abnfDiagram-O67JEVCF-D_6l0gCA.js} +1 -1
- package/dist/web/assets/{agent-store-Grbx4jkz.js → agent-store-Cs-hu1v1.js} +2 -2
- package/dist/web/assets/architecture-7GRP2DOG-KT2-_RA5.js +1 -0
- package/dist/web/assets/{architectureDiagram-NJMV4G6O-BVAJOucG.js → architectureDiagram-NJMV4G6O-BFgjp9nL.js} +1 -1
- package/dist/web/assets/{blockDiagram-BEXU5L5S-TJAfSX44.js → blockDiagram-BEXU5L5S-BWfbJk26.js} +1 -1
- package/dist/web/assets/{c4Diagram-YGBWAQC7-5UHyT_tW.js → c4Diagram-YGBWAQC7-oIqgOwQa.js} +1 -1
- package/dist/web/assets/channel-CAhty-fl.js +1 -0
- package/dist/web/assets/{chunk-3FUC2YCW-CelgLyJi.js → chunk-3FUC2YCW-WI2pliXT.js} +1 -1
- package/dist/web/assets/{chunk-5DYCD2WN-bhEAVIbQ.js → chunk-5DYCD2WN-DYvaF8o2.js} +1 -1
- package/dist/web/assets/{chunk-742MDFTN-ByLz4EcD.js → chunk-742MDFTN-xd7B3x5h.js} +1 -1
- package/dist/web/assets/{chunk-7INBJB4K-CK2i0csa.js → chunk-7INBJB4K-nsyx3RGB.js} +1 -1
- package/dist/web/assets/{chunk-7M6MHVWA-CoCiujjC.js → chunk-7M6MHVWA-BxPQ9AEv.js} +1 -1
- package/dist/web/assets/{chunk-7PRAP22T-CY4NCrCY.js → chunk-7PRAP22T-CvHw2y-F.js} +1 -1
- package/dist/web/assets/{chunk-GTNCS2PH-DMB7bQGB.js → chunk-GTNCS2PH-DIfN1SOI.js} +1 -1
- package/dist/web/assets/{chunk-GWA4HPMP-F6qclPTR.js → chunk-GWA4HPMP-2WZX1fG-.js} +1 -1
- package/dist/web/assets/{chunk-MBY4JIJT-CSiiBtlX.js → chunk-MBY4JIJT-BL7akw7q.js} +1 -1
- package/dist/web/assets/{chunk-NETBCI7D-CBh7GChL.js → chunk-NETBCI7D-CQX1tY3W.js} +1 -1
- package/dist/web/assets/{chunk-O7XYJQB3-B2aqz0sN.js → chunk-O7XYJQB3-CtGXf4J6.js} +1 -1
- package/dist/web/assets/{chunk-UA2S7LBM-B_-P_eR0.js → chunk-UA2S7LBM-C_3M_IIp.js} +1 -1
- package/dist/web/assets/{chunk-WEXAMYUT-19nMFxzK.js → chunk-WEXAMYUT-6MO559WJ.js} +1 -1
- package/dist/web/assets/{chunk-Z7XXMR3K-Co7gzRY1.js → chunk-Z7XXMR3K-CHJ55IlF.js} +1 -1
- package/dist/web/assets/{chunk-ZIGJFQKS-Eoi4K5bK.js → chunk-ZIGJFQKS-533qcfPb.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-NBCMYWYE-CfFGzL-A.js → classDiagram-v2-NBCMYWYE-CFySVswx.js} +1 -1
- package/dist/web/assets/copy-code-XoEu_Yh6.js +1 -0
- package/dist/web/assets/{cynefin-OW5HDTMX-DdZXl3KU.js → cynefin-OW5HDTMX-Dfg_2VTu.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-VND7K2PF-WU21nYH-.js → cynefinDiagram-VND7K2PF-DaSXiPph.js} +1 -1
- package/dist/web/assets/{dagre-6A5THRUB-48z3F8Pl.js → dagre-6A5THRUB-D7d1gbRO.js} +1 -1
- package/dist/web/assets/date-snFAXn1W.js +1 -0
- package/dist/web/assets/{devices-store-Bfk2wdrQ.js → devices-store-DB_tWC1Q.js} +1 -1
- package/dist/web/assets/{diagram-22UHCM2B-B0AZgJW9.js → diagram-22UHCM2B-CNuTss_x.js} +1 -1
- package/dist/web/assets/{diagram-3UASUU5V-CzEzrxuO.js → diagram-3UASUU5V--Rt3-ARV.js} +1 -1
- package/dist/web/assets/{diagram-ATOU4E4O-w6h1bD3w.js → diagram-ATOU4E4O-BP79L-gP.js} +1 -1
- package/dist/web/assets/{diagram-CDSNMT55-BdTHQd_o.js → diagram-CDSNMT55-CRO5cocs.js} +1 -1
- package/dist/web/assets/{diagram-MLGK6HIB-pB4cj6c2.js → diagram-MLGK6HIB-Difu0Zmh.js} +1 -1
- package/dist/web/assets/{diagram-MPIPVDR6-C7C06qyV.js → diagram-MPIPVDR6-grQ3ie17.js} +1 -1
- package/dist/web/assets/{diff-store-DBDL5Hw5.js → diff-store-D4XiTkfi.js} +1 -1
- package/dist/web/assets/{dist-CZz0KXAA.js → dist-75uxNoTf.js} +1 -1
- package/dist/web/assets/{dist-DNpwgDba.js → dist-9rzCziV2.js} +1 -1
- package/dist/web/assets/{dist-BWoPV5P3.js → dist-B9i6cUYu.js} +1 -1
- package/dist/web/assets/{dist-B8i8y3GY.js → dist-BU-fZfEv.js} +1 -1
- package/dist/web/assets/{dist-DWCdhk-O.js → dist-BeUB43Ur.js} +1 -1
- package/dist/web/assets/{dist-BNgPhnKo.js → dist-BxIdk5Al.js} +1 -1
- package/dist/web/assets/{dist-CTJJYvYG.js → dist-By534Syk.js} +1 -1
- package/dist/web/assets/{dist-Dy9K9LOR.js → dist-DFjLCq-0.js} +1 -1
- package/dist/web/assets/{dist-B7Cu0FBW.js → dist-DNrP7O-J.js} +1 -1
- package/dist/web/assets/{dist-DqSUdjG9.js → dist-Du5VPTAq.js} +1 -1
- package/dist/web/assets/{dist-CJ7isxli.js → dist-DyBqP777.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-ZINNZB2B-75vt7539.js → ebnfDiagram-ZINNZB2B-DX2rcvMK.js} +1 -1
- package/dist/web/assets/{elk-276RUBZZ-DXDjJVTy.js → elk-276RUBZZ-DSHIl9hv.js} +1 -1
- package/dist/web/assets/{erDiagram-OPXOYQCR-tNNInTmk.js → erDiagram-OPXOYQCR-BIZVJO8N.js} +1 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-Bh_xDKx6.js +1 -0
- package/dist/web/assets/flowDiagram-KWPJA3E3-DXkIxaXL.js +1 -0
- package/dist/web/assets/{ganttDiagram-FUAMR5RP-h3E8lnCd.js → ganttDiagram-FUAMR5RP-D4kPfh4X.js} +1 -1
- package/dist/web/assets/{gitGraph-4MIJSDKK-BUwY-iv2.js → gitGraph-4MIJSDKK-B41R-oAb.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-X574FWY7-C0AgQH5k.js → gitGraphDiagram-X574FWY7-xXrZiSJI.js} +1 -1
- package/dist/web/assets/i18n-Cfw_OfRR.js +1 -0
- package/dist/web/assets/{icons-BoaZfGBj.js → icons-BIOTvmdT.js} +1 -1
- package/dist/web/assets/index-B1Bugudn.js +2 -0
- package/dist/web/assets/{info-A6RAGUB7-DVMZ4hdK.js → info-A6RAGUB7-CgSmK-La.js} +1 -1
- package/dist/web/assets/{infoDiagram-VRGFBTTK-kLaGXGQ2.js → infoDiagram-VRGFBTTK-CDNEJ_R-.js} +1 -1
- package/dist/web/assets/{init-app-Bxc_l2yT.js → init-app-C3tD3faT.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-Oj7bZlyo.js → ishikawaDiagram-OU5B5YK6-ZcU1IxFn.js} +1 -1
- package/dist/web/assets/{journeyDiagram-ZHPQQLJL-BG7V2Srs.js → journeyDiagram-ZHPQQLJL-g-8ZRhJj.js} +1 -1
- package/dist/web/assets/{kanban-definition-PNTS6WVX-CIJiDE6d.js → kanban-definition-PNTS6WVX-nfsfxVOE.js} +1 -1
- package/dist/web/assets/{line-D2vEKYRZ.js → line-BpuX_ban.js} +1 -1
- package/dist/web/assets/{markdown-Bm2wfvok.js → markdown-ogvtoQzt.js} +1 -1
- package/dist/web/assets/{mdi-Byw1NZrZ.js → mdi-Du7sIqtH.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-BcBEWjW6.js → mermaid-parser.core-BBj_ep7Z.js} +3 -3
- package/dist/web/assets/{mermaid.core-DobO0deo.js → mermaid.core-DP4USVSM.js} +4 -4
- package/dist/web/assets/{mindmap-definition-NLK3R4M7-y7Qz5gBF.js → mindmap-definition-NLK3R4M7-C5GhUvi3.js} +1 -1
- package/dist/web/assets/{packet-AYTQ26CC-oRQgKr9d.js → packet-AYTQ26CC-BIdb-vfg.js} +1 -1
- package/dist/web/assets/{pegDiagram-GJSIUBJH-RIU8XOHq.js → pegDiagram-GJSIUBJH-DH-SdKj6.js} +1 -1
- package/dist/web/assets/{pie-WAS4IAKB-qmOlsLkx.js → pie-WAS4IAKB-Du3-jRjK.js} +1 -1
- package/dist/web/assets/{pieDiagram-5QR66LMP-BM932TDA.js → pieDiagram-5QR66LMP-CJh2ngqY.js} +1 -1
- package/dist/web/assets/pinia-mdReENsc.js +1 -0
- package/dist/web/assets/{preview-store-D7qF6SgX.js → preview-store-DOnSmm_C.js} +1 -1
- package/dist/web/assets/{project-files-store-OhGs_H7U.js → project-files-store-CHP6RYFI.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-O4NWA36T-qQZYwTy7.js → quadrantDiagram-O4NWA36T-DYsSScxc.js} +1 -1
- package/dist/web/assets/{radar-RG4KPBEZ-Kdglk6eY.js → radar-RG4KPBEZ-BBjvaVVB.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-kVhMYx4I.js → railroad-74A4TZTK-B-CRncBU.js} +1 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-D37zpoYN.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-ZnMxH6Ym.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-Dg3g-VsM.js +1 -0
- package/dist/web/assets/{railroadDiagram-XR7U4H2S-K9SrOGcT.js → railroadDiagram-XR7U4H2S-hqp-2UHb.js} +1 -1
- package/dist/web/assets/{requirementDiagram-PLB6GJNP-CtA9ZKEx.js → requirementDiagram-PLB6GJNP-Dhuw5jcQ.js} +1 -1
- package/dist/web/assets/{safe-storage-CvVvrm4v.js → safe-storage-BchoYSbq.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-IPEJSGJF-BH_t4exx.js → sankeyDiagram-IPEJSGJF-D5rCIxGR.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-PO4LG4MO-hPNPqpDa.js → sequenceDiagram-PO4LG4MO-YEHR4Mp-.js} +1 -1
- package/dist/web/assets/{stateDiagram-v2-GCMORJYK-BVmjZDRD.js → stateDiagram-v2-GCMORJYK-9iWmz6Xf.js} +1 -1
- package/dist/web/assets/{swimlanes-2SLR337P-Tgk8-6jj.js → swimlanes-2SLR337P-Cjwvgt_n.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-gJwxhWoO.js +8 -0
- package/dist/web/assets/{terminal-store-CKgJjtC6.js → terminal-store-i7HHlWzR.js} +1 -1
- package/dist/web/assets/theme-CQ9InaWa.js +1 -0
- package/dist/web/assets/{timeline-definition-EJHVYXUP-lAIHEGe8.js → timeline-definition-EJHVYXUP-Cd5n2-Py.js} +1 -1
- package/dist/web/assets/{treeView-Q6P3EWNA-CdvzKtWf.js → treeView-Q6P3EWNA-C_QH9vtR.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-BxDSJfrJ.js → treemap-WGGIJYW6-CVnU7TVz.js} +1 -1
- package/dist/web/assets/{use-add-agent-context-BSHGL5iV.js → use-add-agent-context-D5VqMr0p.js} +1 -1
- package/dist/web/assets/use-app-theme-DgC4ivPF.js +1 -0
- package/dist/web/assets/use-row-height-BgHg4-J-.js +1 -0
- package/dist/web/assets/{use-workspace-navigation-AaRMWmjQ.js → use-workspace-navigation-Dq-Aw8Xq.js} +1 -1
- package/dist/web/assets/{usecaseDiagram-POWQR4AR-BH3KvMpz.js → usecaseDiagram-POWQR4AR-Cbj2Pvab.js} +1 -1
- package/dist/web/assets/{vennDiagram-UO4OBE2U-Dfi-raKp.js → vennDiagram-UO4OBE2U-2j79YqCN.js} +1 -1
- package/dist/web/assets/{wardley-WFR3VGLG-DmbtSM24.js → wardley-WFR3VGLG-wsMK7zm3.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VNRHLVJA-DIzhGvI7.js → wardleyDiagram-VNRHLVJA-D6PKTQIJ.js} +1 -1
- package/dist/web/assets/{workspace-navigation-xfxULfM7.js → workspace-navigation-CoXKJJpP.js} +1 -1
- package/dist/web/assets/{xychartDiagram-PMCCYNJV-XgqSHH_N.js → xychartDiagram-PMCCYNJV-D1fQN7KG.js} +1 -1
- package/dist/web/index.html +2 -1
- package/package.json +1 -1
- package/dist/web/assets/AddToAgentButton-BitTdCEg.js +0 -1
- package/dist/web/assets/ArtifactDefinitionPicker-BQOoU60k.js +0 -3
- package/dist/web/assets/ArtifactsPage-BJR-XYYs.js +0 -2
- package/dist/web/assets/ClosePopup-BtDeDZsk.js +0 -1
- package/dist/web/assets/DialogHeader-CQvAZVx7.js +0 -1
- package/dist/web/assets/IndexPage-DMTZyWQA.js +0 -1
- package/dist/web/assets/MainLayout-DWJ_-EMV.js +0 -7
- package/dist/web/assets/NotesPage-CjaxjLBW.js +0 -4
- package/dist/web/assets/QFab-CxpedhPI.js +0 -1
- package/dist/web/assets/QScrollArea-js5dkUz1.js +0 -1
- package/dist/web/assets/QTabs-DIy5gWMb.js +0 -1
- package/dist/web/assets/QVirtualScroll-DkD0TczV.js +0 -1
- package/dist/web/assets/ReviewDetailPage-Bmc8DmSu.js +0 -1
- package/dist/web/assets/ReviewsPage-DR1IWCmu.js +0 -1
- package/dist/web/assets/StatusSwitcher-CpkH8BuC.js +0 -1
- package/dist/web/assets/architecture-7GRP2DOG-DmaYCP8T.js +0 -1
- package/dist/web/assets/channel-B4_5H-fb.js +0 -1
- package/dist/web/assets/copy-code-DaqEfdXL.js +0 -1
- package/dist/web/assets/date-DZIuZp-w.js +0 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-QMRehQtM.js +0 -1
- package/dist/web/assets/flowDiagram-KWPJA3E3-DJAgsbXC.js +0 -1
- package/dist/web/assets/i18n-CfTd46OX.js +0 -1
- package/dist/web/assets/index-BTpokWxr.js +0 -2
- package/dist/web/assets/pinia-Cmp1GI8k.js +0 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-MiKgjOXr.js +0 -1
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-BFL9h7Zx.js +0 -1
- package/dist/web/assets/railroad-peg-WCYAUIDC-ChGGfIn7.js +0 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-W1zDrK61.js +0 -8
- package/dist/web/assets/theme-slSxLNv6.js +0 -1
- package/dist/web/assets/use-app-theme-fmVV2QM8.js +0 -1
- package/dist/web/assets/use-panel-width-BYOPuHat.js +0 -1
- package/dist/web/assets/use-row-height-BK8kNihW.js +0 -1
package/README.md
CHANGED
|
@@ -197,6 +197,18 @@ node dist/cli/index.js list --db-prefix "" # .githuman/reviews.db (ор
|
|
|
197
197
|
`--api-docs` (`pnpm run dev:server` включает его сам). Агентские маршруты
|
|
198
198
|
попадают в спецификацию, только когда сервер их отдаёт (loopback-хост).
|
|
199
199
|
|
|
200
|
+
### Отладка валидации артефактов
|
|
201
|
+
|
|
202
|
+
`githuman serve --debug-validation` не закрывает сессию валидатора после
|
|
203
|
+
завершения проверки и пишет в консоль сервера строки `[validation <id>]`:
|
|
204
|
+
статусы сессии, вызовы инструментов, решения политики прав (ALLOW/DENY с
|
|
205
|
+
причиной), причину завершения. Текст вывода инструментов обрезается до 500
|
|
206
|
+
символов. Флаг нужен для отладки: в обычном режиме сессия закрывается, а
|
|
207
|
+
причина остановки валидатора нигде не сохраняется. Оставленная открытой
|
|
208
|
+
сессия не занимает место в лимите чатов: новый чат закрывает самую старую
|
|
209
|
+
завершённую сессию валидатора. Ключ артефактной сессии такого валидатора
|
|
210
|
+
отзывается сразу при завершении проверки.
|
|
211
|
+
|
|
200
212
|
## Разработка
|
|
201
213
|
|
|
202
214
|
Два процесса нужны одновременно — backend (Fastify, автоперезапуск при
|
package/dist/cli/config.js
CHANGED
|
@@ -88,6 +88,7 @@ export function parseServeArgs(argv) {
|
|
|
88
88
|
'notes-dir': { type: 'string' },
|
|
89
89
|
'db-prefix': { type: 'string' },
|
|
90
90
|
'api-docs': { type: 'boolean', default: false },
|
|
91
|
+
'debug-validation': { type: 'boolean', default: false },
|
|
91
92
|
'allowed-host': { type: 'string', multiple: true }
|
|
92
93
|
},
|
|
93
94
|
strict: false
|
|
@@ -101,6 +102,7 @@ export function parseServeArgs(argv) {
|
|
|
101
102
|
throw new Error(`Invalid --db-prefix value "${dbPrefix}": use at most 100 characters and none of < > : " / \\ | ? * or control characters. Set it with --db-prefix or GITHUMAN_DB_PREFIX.`);
|
|
102
103
|
}
|
|
103
104
|
const apiDocs = values['api-docs'] === true;
|
|
105
|
+
const debugValidation = values['debug-validation'] === true;
|
|
104
106
|
const allowedHosts = resolveAllowedHosts(Array.isArray(values['allowed-host']) ? values['allowed-host'] : []);
|
|
105
107
|
return {
|
|
106
108
|
port,
|
|
@@ -108,6 +110,7 @@ export function parseServeArgs(argv) {
|
|
|
108
110
|
open,
|
|
109
111
|
dbPrefix,
|
|
110
112
|
apiDocs,
|
|
113
|
+
debugValidation,
|
|
111
114
|
allowedHosts,
|
|
112
115
|
...(typeof values['notes-dir'] === 'string'
|
|
113
116
|
? { notesDir: values['notes-dir'] }
|
|
@@ -69,6 +69,10 @@ export async function startServer(options) {
|
|
|
69
69
|
artifactPrefix: resolveDbPrefix(options.dbPrefix),
|
|
70
70
|
watchFiles: true,
|
|
71
71
|
apiDocs: options.apiDocs === true,
|
|
72
|
+
// The server logs are off; validation debug lines go to the console.
|
|
73
|
+
...(options.debugValidation === true
|
|
74
|
+
? { onValidationDebug: (line) => console.log(line) }
|
|
75
|
+
: {}),
|
|
72
76
|
allowedHosts: options.allowedHosts ?? [],
|
|
73
77
|
onPairingRequest: device => console.log(`Pairing request from ${device.name} (${device.address}). Enter the code from the device in a localhost tab.`),
|
|
74
78
|
terminalEnabled: true,
|
package/dist/server/app.js
CHANGED
|
@@ -163,6 +163,9 @@ export function buildApp(options = {}) {
|
|
|
163
163
|
});
|
|
164
164
|
artifacts.lifecycle.transitions.setActorGuard(actor => actor.id === 'local' || devices.isTrusted(actor.id));
|
|
165
165
|
artifacts.lifecycle.transitions.setErrorHandler(error => app.log.error({ err: error }, 'Artifact transition work failed'));
|
|
166
|
+
if (options.onValidationDebug) {
|
|
167
|
+
artifacts.lifecycle.jobs.setDebug(options.onValidationDebug);
|
|
168
|
+
}
|
|
166
169
|
app.register(artifactTransitionRoutes, {
|
|
167
170
|
service: artifacts.lifecycle.transitions,
|
|
168
171
|
recheckAccess: createAccessHook({ devices, allowedHosts })
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { isLoopbackHost } from "../../../shared/network/loopback.js";
|
|
2
2
|
import { forwardedAddresses, isLoopbackAddress, parseHost, sameOrigin } from "../../utils/request-origin.js";
|
|
3
|
+
/** True when the `Host` header names this machine (`localhost`, `127.0.0.1`, `[::1]`). */
|
|
4
|
+
export function isLoopbackHostHeader(host) {
|
|
5
|
+
const target = parseHost(host);
|
|
6
|
+
return target !== undefined && isLoopbackHost(target.hostname);
|
|
7
|
+
}
|
|
3
8
|
/**
|
|
4
9
|
* True when the request comes from a page or a client on this machine
|
|
5
10
|
* (ADR 0079). The `Host` header alone is not proof: with a bind on
|
|
@@ -2,7 +2,7 @@ import { ARTIFACT_BRIDGE_ROUTE } from "../../../shared/artifacts/mcp-constants.j
|
|
|
2
2
|
import { DEVICE_IDLE_TTL_MS } from "../../../shared/devices/constants.js";
|
|
3
3
|
import { ForbiddenError, UnauthorizedError } from "../../errors/http.js";
|
|
4
4
|
import { clientAddress, sameOrigin } from "../../utils/request-origin.js";
|
|
5
|
-
import { isLocalRequest } from "./local-request.js";
|
|
5
|
+
import { isLocalRequest, isLoopbackHostHeader } from "./local-request.js";
|
|
6
6
|
// Methods that do not change state; CSRF checks the others.
|
|
7
7
|
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
8
8
|
// API routes that a browser without access needs to request pairing.
|
|
@@ -50,7 +50,10 @@ export function createAccessHook(options) {
|
|
|
50
50
|
const { host, origin } = request.headers;
|
|
51
51
|
const local = isLocalRequest(request);
|
|
52
52
|
if (!local && !allowedHosts.allows(host)) {
|
|
53
|
-
|
|
53
|
+
// A loopback Host that fails here has a non-local Origin, proxy address or peer.
|
|
54
|
+
throw new ForbiddenError(isLoopbackHostHeader(host)
|
|
55
|
+
? `The request to Host "${host}" is not local: its Origin, X-Forwarded-For or peer address is not on this machine.`
|
|
56
|
+
: `Unknown Host "${host ?? '-'}". Open the app by the IP address of this machine, or start the server with --allowed-host.`);
|
|
54
57
|
}
|
|
55
58
|
if (origin !== undefined &&
|
|
56
59
|
!SAFE_METHODS.has(request.method) &&
|
|
@@ -118,6 +118,17 @@ export class ArtifactValidationRepository {
|
|
|
118
118
|
.prepare('INSERT INTO artifact_authorization_events(store_id,id,kind,payload_json) VALUES(?,?,?,?)')
|
|
119
119
|
.run(this.storeId, id, kind, JSON.stringify(data));
|
|
120
120
|
}
|
|
121
|
+
/** What the revoke of a pending action left behind: who asked and when it would expire. */
|
|
122
|
+
revoked(id) {
|
|
123
|
+
const row = this.db
|
|
124
|
+
.prepare("SELECT payload_json FROM artifact_authorization_events WHERE store_id=? AND id=? AND kind='revoked' ORDER BY seq DESC LIMIT 1")
|
|
125
|
+
.get(this.storeId, id);
|
|
126
|
+
if (!row) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const value = JSON.parse(row.payload_json);
|
|
130
|
+
return typeof value === 'object' && value !== null ? value : {};
|
|
131
|
+
}
|
|
121
132
|
consumed(id) {
|
|
122
133
|
return Boolean(this.db
|
|
123
134
|
.prepare("SELECT 1 FROM artifact_authorization_events WHERE store_id=? AND id=? AND kind='consumed'")
|
|
@@ -19,6 +19,10 @@ export async function artifactAgentRoutes(app, { storage, registry }) {
|
|
|
19
19
|
'artifacts.search',
|
|
20
20
|
checked(ArtifactToolSchemas['artifacts.search'], p => storage.search.search(p))
|
|
21
21
|
],
|
|
22
|
+
[
|
|
23
|
+
'change_sets.preview',
|
|
24
|
+
checked(ArtifactToolSchemas['change_sets.preview'], p => storage.graph.previewChangeSet(p))
|
|
25
|
+
],
|
|
22
26
|
[
|
|
23
27
|
'change_sets.propose',
|
|
24
28
|
checked(ArtifactToolSchemas['change_sets.propose'], (p, a) => storage.lifecycle.propose(p, a))
|
|
@@ -41,7 +45,11 @@ export async function artifactAgentRoutes(app, { storage, registry }) {
|
|
|
41
45
|
],
|
|
42
46
|
[
|
|
43
47
|
'validations.report',
|
|
44
|
-
checked(ArtifactToolSchemas['validations.report'],
|
|
48
|
+
checked(ArtifactToolSchemas['validations.report'], (p, a) => {
|
|
49
|
+
const result = storage.lifecycle.jobs.report(p.id, p.offset);
|
|
50
|
+
storage.lifecycle.jobs.observe(p.id, a.id);
|
|
51
|
+
return result;
|
|
52
|
+
})
|
|
45
53
|
],
|
|
46
54
|
[
|
|
47
55
|
'validations.history',
|
|
@@ -49,7 +57,11 @@ export async function artifactAgentRoutes(app, { storage, registry }) {
|
|
|
49
57
|
],
|
|
50
58
|
[
|
|
51
59
|
'validations.get',
|
|
52
|
-
checked(ArtifactToolSchemas['validations.get'], p =>
|
|
60
|
+
checked(ArtifactToolSchemas['validations.get'], (p, a) => {
|
|
61
|
+
const result = storage.lifecycle.jobs.get(p.id);
|
|
62
|
+
storage.lifecycle.jobs.observe(p.id, a.id);
|
|
63
|
+
return result;
|
|
64
|
+
})
|
|
53
65
|
],
|
|
54
66
|
[
|
|
55
67
|
'validations.manifest',
|
|
@@ -59,6 +71,10 @@ export async function artifactAgentRoutes(app, { storage, registry }) {
|
|
|
59
71
|
'validations.read',
|
|
60
72
|
checked(ArtifactToolSchemas['validations.read'], (p, a) => storage.lifecycle.jobs.read(p.id, a.id, p.key, p.offset))
|
|
61
73
|
],
|
|
74
|
+
[
|
|
75
|
+
'validations.read_next',
|
|
76
|
+
checked(ArtifactToolSchemas['validations.read_next'], (p, a) => storage.lifecycle.jobs.readNext(p.id, a.id))
|
|
77
|
+
],
|
|
62
78
|
[
|
|
63
79
|
'validations.submit',
|
|
64
80
|
checked(ArtifactToolSchemas['validations.submit'], ({ id, ...p }, a) => storage.lifecycle.jobs.submit(id, a.id, p))
|
|
@@ -11,6 +11,8 @@ import { describePreset } from "../../config/agents.js";
|
|
|
11
11
|
export function createSessionRegistry(options) {
|
|
12
12
|
const sessions = new Map();
|
|
13
13
|
const unsubscribeSession = new Map();
|
|
14
|
+
// Finished sessions kept open for inspection; a new session may replace them.
|
|
15
|
+
const retired = new Set();
|
|
14
16
|
const listeners = new Set();
|
|
15
17
|
const keys = new Map();
|
|
16
18
|
const sessionKeys = new Map();
|
|
@@ -70,6 +72,17 @@ export function createSessionRegistry(options) {
|
|
|
70
72
|
unsubscribeSession.get(id)?.();
|
|
71
73
|
unsubscribeSession.delete(id);
|
|
72
74
|
sessions.delete(id);
|
|
75
|
+
retired.delete(id);
|
|
76
|
+
}
|
|
77
|
+
function closeSession(id) {
|
|
78
|
+
const session = sessions.get(id);
|
|
79
|
+
if (!session) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
session.close();
|
|
83
|
+
drop(id);
|
|
84
|
+
broadcast({ type: 'sessions' });
|
|
85
|
+
return true;
|
|
73
86
|
}
|
|
74
87
|
function closeAll() {
|
|
75
88
|
for (const session of sessions.values()) {
|
|
@@ -118,6 +131,13 @@ export function createSessionRegistry(options) {
|
|
|
118
131
|
if (!preset) {
|
|
119
132
|
throw new UnknownAgentPresetError(`Unknown agent "${presetId}"`);
|
|
120
133
|
}
|
|
134
|
+
if (sessions.size >= MAX_AGENT_SESSIONS) {
|
|
135
|
+
// A Set keeps insertion order, so the first retired session is the oldest.
|
|
136
|
+
const [oldest] = retired;
|
|
137
|
+
if (oldest !== undefined) {
|
|
138
|
+
closeSession(oldest);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
121
141
|
if (sessions.size >= MAX_AGENT_SESSIONS) {
|
|
122
142
|
throw new TooManyAgentSessionsError(`At most ${MAX_AGENT_SESSIONS} chats can be open at once`);
|
|
123
143
|
}
|
|
@@ -163,16 +183,18 @@ export function createSessionRegistry(options) {
|
|
|
163
183
|
return session;
|
|
164
184
|
},
|
|
165
185
|
get: id => sessions.get(id),
|
|
166
|
-
list: () => [...sessions.values()].map(session =>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
186
|
+
list: () => [...sessions.values()].map(session => ({
|
|
187
|
+
...session.info(),
|
|
188
|
+
...(retired.has(session.id) ? { retired: true } : {})
|
|
189
|
+
})),
|
|
190
|
+
close: closeSession,
|
|
191
|
+
retire(id) {
|
|
192
|
+
if (sessions.has(id)) {
|
|
193
|
+
retired.add(id);
|
|
194
|
+
// A retired validator stays open for inspection, but its key must stop working.
|
|
195
|
+
revoke(id);
|
|
196
|
+
broadcast({ type: 'sessions' });
|
|
171
197
|
}
|
|
172
|
-
session.close();
|
|
173
|
-
drop(id);
|
|
174
|
-
broadcast({ type: 'sessions' });
|
|
175
|
-
return true;
|
|
176
198
|
},
|
|
177
199
|
closeAll,
|
|
178
200
|
subscribe(listener, afterId = 0) {
|
|
@@ -142,6 +142,26 @@ export class ArtifactAuthorizationService {
|
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
144
|
status(id, sessionId) {
|
|
145
|
+
const known = this.pending.get(id) ??
|
|
146
|
+
[...this.grants.values()].find(value => value.pendingActionId === id);
|
|
147
|
+
// The requester learns how its request ended, not only that it is gone.
|
|
148
|
+
if (known?.sessionId === sessionId && known.expiresAt <= this.now()) {
|
|
149
|
+
return {
|
|
150
|
+
id,
|
|
151
|
+
state: 'expired',
|
|
152
|
+
authorizationId: null,
|
|
153
|
+
expiresAt: known.expiresAt
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const revoked = known ? null : this.repository.revoked(id);
|
|
157
|
+
if (revoked?.sessionId === sessionId) {
|
|
158
|
+
return {
|
|
159
|
+
id,
|
|
160
|
+
state: 'revoked',
|
|
161
|
+
authorizationId: null,
|
|
162
|
+
expiresAt: revoked.expiresAt ?? this.now()
|
|
163
|
+
};
|
|
164
|
+
}
|
|
145
165
|
const value = this.describe(id);
|
|
146
166
|
if (value.pending.sessionId !== sessionId)
|
|
147
167
|
throw new ArtifactNotFoundError();
|
|
@@ -163,9 +183,14 @@ export class ArtifactAuthorizationService {
|
|
|
163
183
|
at: this.now()
|
|
164
184
|
});
|
|
165
185
|
}
|
|
186
|
+
const target = this.pending.get(id) ?? grant;
|
|
166
187
|
this.grants.delete(id);
|
|
167
188
|
this.pending.delete(id);
|
|
168
|
-
this.repository.authorizationEvent(id, 'revoked', {
|
|
189
|
+
this.repository.authorizationEvent(id, 'revoked', {
|
|
190
|
+
at: this.now(),
|
|
191
|
+
sessionId: target?.sessionId,
|
|
192
|
+
expiresAt: target?.expiresAt
|
|
193
|
+
});
|
|
169
194
|
this.changed();
|
|
170
195
|
}
|
|
171
196
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { canonicalJson } from "../../shared/utils/canonical-json.js";
|
|
2
2
|
import { sha256Hex } from "../utils/hash.js";
|
|
3
3
|
import { allowedTransition } from "../../shared/artifact-transitions/transitions.js";
|
|
4
|
+
// A large store can give hundreds of issues. The full list is in the change set preview.
|
|
5
|
+
const MAX_REPORTED_GRAPH_ISSUES = 20;
|
|
6
|
+
// Limit for artifact IDs in one issue line; the rest show as a count.
|
|
7
|
+
const MAX_REPORTED_ISSUE_ARTIFACTS = 20;
|
|
4
8
|
// Status that a lifecycle command gives to each of its subjects.
|
|
5
9
|
const COMMAND_STATUS = {
|
|
6
10
|
return_to_draft: 'draft',
|
|
@@ -10,6 +14,37 @@ const COMMAND_STATUS = {
|
|
|
10
14
|
delete: 'deleted',
|
|
11
15
|
restore: 'draft'
|
|
12
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Builds one error text that names the artifacts of each issue.
|
|
19
|
+
* Without the IDs and current versions, a caller cannot correct the change set.
|
|
20
|
+
* Issues with the same code and text merge into one line.
|
|
21
|
+
*/
|
|
22
|
+
export function graphIssuesMessage(issues, versions = []) {
|
|
23
|
+
const current = new Map(versions.map(item => [item.id, item.version]));
|
|
24
|
+
const groups = new Map();
|
|
25
|
+
for (const issue of issues) {
|
|
26
|
+
const key = `${issue.code}\0${issue.message}`;
|
|
27
|
+
const group = groups.get(key) ?? { issue, ids: new Set() };
|
|
28
|
+
for (const id of issue.artifactIds)
|
|
29
|
+
group.ids.add(id);
|
|
30
|
+
groups.set(key, group);
|
|
31
|
+
}
|
|
32
|
+
const lines = [...groups.values()].map(({ issue, ids }) => {
|
|
33
|
+
const all = [...ids].map(id => current.has(id) ? `${id}@v${current.get(id)}` : id);
|
|
34
|
+
const shown = all.slice(0, MAX_REPORTED_ISSUE_ARTIFACTS);
|
|
35
|
+
if (all.length > shown.length) {
|
|
36
|
+
shown.push(`+${all.length - shown.length} more`);
|
|
37
|
+
}
|
|
38
|
+
return shown.length
|
|
39
|
+
? `${issue.code} [${shown.join(', ')}]: ${issue.message}`
|
|
40
|
+
: `${issue.code}: ${issue.message}`;
|
|
41
|
+
});
|
|
42
|
+
const shown = lines.slice(0, MAX_REPORTED_GRAPH_ISSUES);
|
|
43
|
+
if (lines.length > shown.length) {
|
|
44
|
+
shown.push(`${lines.length - shown.length} more issues. Preview the change set for the full list.`);
|
|
45
|
+
}
|
|
46
|
+
return shown.join(' ');
|
|
47
|
+
}
|
|
13
48
|
// Each kind is a separate graph that must stay acyclic.
|
|
14
49
|
function edgeTargets(kind, artifact) {
|
|
15
50
|
if (kind === 'lineage')
|
|
@@ -399,7 +434,7 @@ export function previewArtifactChangeSet(current, input, previouslySuperseded =
|
|
|
399
434
|
replacements.set(relation.targetId, node.id);
|
|
400
435
|
}
|
|
401
436
|
}
|
|
402
|
-
|
|
437
|
+
const postIssues = graphIssues([...post.values()], canonical.storeId);
|
|
403
438
|
// Compute the component in the union of the old and proposed graphs.
|
|
404
439
|
// A references target enters the set for existence checks only. It does not expand the set.
|
|
405
440
|
const referenceTargets = new Set();
|
|
@@ -407,7 +442,8 @@ export function previewArtifactChangeSet(current, input, previouslySuperseded =
|
|
|
407
442
|
while (changed) {
|
|
408
443
|
changed = false;
|
|
409
444
|
const add = (id, viaReference = false) => {
|
|
410
|
-
|
|
445
|
+
// Deleted documents were checked when deleted; graph rules still see their metadata.
|
|
446
|
+
if (!nodes.has(id) || nodes.get(id).status === 'deleted')
|
|
411
447
|
return;
|
|
412
448
|
if (!affected.has(id)) {
|
|
413
449
|
affected.add(id);
|
|
@@ -443,12 +479,17 @@ export function previewArtifactChangeSet(current, input, previouslySuperseded =
|
|
|
443
479
|
add(relation.targetId, true);
|
|
444
480
|
}
|
|
445
481
|
}
|
|
482
|
+
// An accepted document of the same task is context only: it does not expand the set (ADR 0119).
|
|
446
483
|
if (node.status === 'accepted' &&
|
|
447
|
-
|
|
448
|
-
add(node.id);
|
|
484
|
+
node.taskIds.some(id => tasks.has(id))) {
|
|
485
|
+
add(node.id, true);
|
|
449
486
|
}
|
|
450
487
|
}
|
|
451
488
|
}
|
|
489
|
+
// An old issue in an unrelated part of the store does not block this change set.
|
|
490
|
+
const before = new Set(graphIssues(current, canonical.storeId).map(issueKey));
|
|
491
|
+
errors.push(...postIssues.filter(issue => !before.has(issueKey(issue)) ||
|
|
492
|
+
issue.artifactIds.some(id => affected.has(id))));
|
|
452
493
|
for (const expected of canonical.expectedVersions) {
|
|
453
494
|
if (nodes.get(expected.id)?.metadataVersion !== expected.version) {
|
|
454
495
|
error('stale_version', [expected.id], 'Expected artifact version no longer matches.');
|
|
@@ -484,6 +525,9 @@ export function previewArtifactChangeSet(current, input, previouslySuperseded =
|
|
|
484
525
|
includedIn
|
|
485
526
|
};
|
|
486
527
|
}
|
|
528
|
+
function issueKey(issue) {
|
|
529
|
+
return canonicalJson([issue.code, issue.artifactIds]);
|
|
530
|
+
}
|
|
487
531
|
function isAncestor(id, artifact, nodes) {
|
|
488
532
|
const visited = new Set();
|
|
489
533
|
let parent = artifact.revisionOf[0];
|
|
@@ -7,17 +7,31 @@ import { sha256Hex } from "../utils/hash.js";
|
|
|
7
7
|
import { HEARTBEAT_STALE_MS } from "../../shared/artifact-transitions/constants.js";
|
|
8
8
|
import { hasErrorCode } from "../utils/system-error.js";
|
|
9
9
|
import { ArtifactConflictError, ArtifactInputError } from "../errors/artifacts.js";
|
|
10
|
-
import { canonicalChangeSet, previewArtifactChangeSet } from "./artifact-graph-validation.js";
|
|
10
|
+
import { canonicalChangeSet, graphIssuesMessage, previewArtifactChangeSet } from "./artifact-graph-validation.js";
|
|
11
11
|
import { ArtifactValidationRepository } from "../repositories/artifact-validation.repo.js";
|
|
12
12
|
import { ArtifactAuthorizationService, artifactActionDigest } from "./artifact-authorization.service.js";
|
|
13
13
|
import { ArtifactValidationJobsService } from "./artifact-validation-jobs.service.js";
|
|
14
14
|
import { ArtifactTransitionRepository } from "../repositories/artifact-transition.repo.js";
|
|
15
15
|
import { ArtifactTransitionService } from "./artifact-transition.service.js";
|
|
16
16
|
// Manifest changes invalidate all earlier semantic conclusions.
|
|
17
|
-
const VALIDATION_RULES_VERSION =
|
|
17
|
+
const VALIDATION_RULES_VERSION = 3;
|
|
18
|
+
// The server is the author of a proposal version that only follows the grown set (ADR 0121).
|
|
19
|
+
const REBASE_ACTOR = { kind: 'system', id: 'validation-rebase' };
|
|
20
|
+
// Preview errors that a rebase can fix: they only name versions of the set context.
|
|
21
|
+
const REBASE_CODES = new Set(['missing_expected_version', 'stale_version']);
|
|
22
|
+
// The reason in the request list is cut to this length; the full text stays in the proposal.
|
|
23
|
+
const ACTION_REASON_CHARS = 300;
|
|
18
24
|
export function artifactSessionId(actor) {
|
|
19
25
|
return actor.kind === 'user' ? `user:${actor.id}` : actor.id;
|
|
20
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The digest covers the affected set, not the store epoch. A store change outside
|
|
29
|
+
* the set does not make a running validation stale (ADR 0119).
|
|
30
|
+
*/
|
|
31
|
+
function setDigest(manifest) {
|
|
32
|
+
const { validationEpoch: _epoch, ...content } = manifest;
|
|
33
|
+
return artifactActionDigest(content);
|
|
34
|
+
}
|
|
21
35
|
export class ArtifactLifecycleService {
|
|
22
36
|
repository;
|
|
23
37
|
authorization;
|
|
@@ -89,7 +103,7 @@ export class ArtifactLifecycleService {
|
|
|
89
103
|
}
|
|
90
104
|
const preview = this.preview(input.changeSet);
|
|
91
105
|
if (preview.errors.length) {
|
|
92
|
-
throw new ArtifactInputError(preview.errors
|
|
106
|
+
throw new ArtifactInputError(graphIssuesMessage(preview.errors, preview.expectedVersions));
|
|
93
107
|
}
|
|
94
108
|
const previous = input.id ? this.repository.proposal(input.id) : undefined;
|
|
95
109
|
if ((previous && previous.version !== input.expectedVersion) ||
|
|
@@ -137,6 +151,26 @@ export class ArtifactLifecycleService {
|
|
|
137
151
|
};
|
|
138
152
|
});
|
|
139
153
|
}
|
|
154
|
+
/** The pending requests, each with a short summary so the user can tell them apart. */
|
|
155
|
+
listActions() {
|
|
156
|
+
return this.storage.catalog.readSnapshot(() => this.authorization.list().map(pending => {
|
|
157
|
+
try {
|
|
158
|
+
const { changeSet } = this.repository.proposalVersion(pending.action.changeSetId, pending.action.version);
|
|
159
|
+
return {
|
|
160
|
+
...pending,
|
|
161
|
+
summary: {
|
|
162
|
+
command: changeSet.command,
|
|
163
|
+
reason: changeSet.reason.slice(0, ACTION_REASON_CHARS),
|
|
164
|
+
documentCount: changeSet.targetStatuses.length
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// A proposal that cannot be read must not hide the request itself.
|
|
170
|
+
return { ...pending, summary: null };
|
|
171
|
+
}
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
140
174
|
inspectAction(id, actor) {
|
|
141
175
|
return this.storage.catalog.readSnapshot(() => {
|
|
142
176
|
const value = this.authorization.describe(id);
|
|
@@ -204,7 +238,7 @@ export class ArtifactLifecycleService {
|
|
|
204
238
|
changeSetId,
|
|
205
239
|
version,
|
|
206
240
|
manifest,
|
|
207
|
-
manifestDigest:
|
|
241
|
+
manifestDigest: setDigest(manifest),
|
|
208
242
|
state: 'prepared',
|
|
209
243
|
validatorSessionId: null,
|
|
210
244
|
owner: null,
|
|
@@ -216,7 +250,7 @@ export class ArtifactLifecycleService {
|
|
|
216
250
|
manifest(proposal) {
|
|
217
251
|
const preview = this.preview(proposal.changeSet);
|
|
218
252
|
if (preview.errors.length) {
|
|
219
|
-
throw new ArtifactConflictError(preview.errors
|
|
253
|
+
throw new ArtifactConflictError(graphIssuesMessage(preview.errors, preview.expectedVersions));
|
|
220
254
|
}
|
|
221
255
|
const artifacts = preview.affectedIds.map(id => this.storage.readVerifiedFile(id).artifact);
|
|
222
256
|
return {
|
|
@@ -228,12 +262,66 @@ export class ArtifactLifecycleService {
|
|
|
228
262
|
requiredContentIds: artifacts.map(item => item.id)
|
|
229
263
|
};
|
|
230
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Gives the next proposal version and manifest of a running validation when only
|
|
267
|
+
* the context of its set changed (ADR 0121). The caller owns the transaction.
|
|
268
|
+
* Returns undefined when the change needs a decision of the proposal author.
|
|
269
|
+
*/
|
|
270
|
+
rebaseInTransaction(validation) {
|
|
271
|
+
const proposal = this.repository.proposal(validation.changeSetId);
|
|
272
|
+
if (proposal.version !== validation.version ||
|
|
273
|
+
validation.manifest.rulesVersion !== VALIDATION_RULES_VERSION) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
const subjects = new Set(proposal.changeSet.subjectIds);
|
|
277
|
+
const preview = this.preview(proposal.changeSet);
|
|
278
|
+
const decided = proposal.changeSet.supplementDispositions
|
|
279
|
+
.map(item => item.artifactId)
|
|
280
|
+
.sort();
|
|
281
|
+
if (!preview.errors.every(issue => REBASE_CODES.has(issue.code) &&
|
|
282
|
+
issue.artifactIds.every(id => !subjects.has(id))) ||
|
|
283
|
+
canonicalJson(preview.requiredSupplementDispositions) !==
|
|
284
|
+
canonicalJson(decided)) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
const command = proposal.changeSet.command;
|
|
288
|
+
// A historical command has no current proposal form; its validation stays stale.
|
|
289
|
+
if (command === 'submit_review') {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
let next = proposal;
|
|
293
|
+
if (preview.errors.length) {
|
|
294
|
+
next = this.proposeInTransaction({
|
|
295
|
+
requestId: `validation-rebase:${validation.id}:${proposal.version}`,
|
|
296
|
+
id: proposal.id,
|
|
297
|
+
expectedVersion: proposal.version,
|
|
298
|
+
changeSet: {
|
|
299
|
+
...proposal.changeSet,
|
|
300
|
+
command,
|
|
301
|
+
expectedVersions: preview.expectedVersions
|
|
302
|
+
}
|
|
303
|
+
}, REBASE_ACTOR);
|
|
304
|
+
}
|
|
305
|
+
const manifest = this.manifest(next);
|
|
306
|
+
return { proposal: next, manifest, digest: setDigest(manifest) };
|
|
307
|
+
}
|
|
231
308
|
assertFresh(validation) {
|
|
232
309
|
const proposal = this.repository.proposal(validation.changeSetId);
|
|
310
|
+
let digest;
|
|
311
|
+
try {
|
|
312
|
+
digest = setDigest(this.manifest(proposal));
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
// A store change can make the change set invalid, for example a new accepted
|
|
316
|
+
// artifact without tasks. That is staleness too, so the caller sees one error.
|
|
317
|
+
if (!(error instanceof ArtifactConflictError)) {
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
throw new ArtifactConflictError(`VALIDATION_STALE: manifest changed (${error.message}); prepare a new validation.`);
|
|
321
|
+
}
|
|
233
322
|
if (validation.manifest.rulesVersion !== VALIDATION_RULES_VERSION ||
|
|
234
323
|
proposal.version !== validation.version ||
|
|
235
|
-
|
|
236
|
-
validation.manifestDigest) {
|
|
324
|
+
digest !== validation.manifestDigest) {
|
|
237
325
|
throw new ArtifactConflictError('VALIDATION_STALE: manifest changed; prepare a new validation.');
|
|
238
326
|
}
|
|
239
327
|
return proposal;
|
|
@@ -4,7 +4,7 @@ import { takeWithinBytes } from "../utils/take-within-bytes.js";
|
|
|
4
4
|
import { ArtifactTypeService } from "./artifact-type.service.js";
|
|
5
5
|
import { TaskService } from "./task.service.js";
|
|
6
6
|
import { ArtifactGraphService } from "./artifact-graph.service.js";
|
|
7
|
-
import { artifactIssues } from "./artifact-graph-validation.js";
|
|
7
|
+
import { artifactIssues, graphIssuesMessage } from "./artifact-graph-validation.js";
|
|
8
8
|
import { ArtifactGraphRepository } from "../repositories/artifact-graph.repo.js";
|
|
9
9
|
import { TaskRepository } from "../repositories/task.repo.js";
|
|
10
10
|
import { normalizeArtifact } from "../../shared/artifacts/normalize.js";
|
|
@@ -622,7 +622,7 @@ export class ArtifactStorageService {
|
|
|
622
622
|
}
|
|
623
623
|
const issues = artifactIssues(artifact, id => this.catalog.get(id), this.storeId);
|
|
624
624
|
if (issues.length) {
|
|
625
|
-
throw new ArtifactInputError(issues
|
|
625
|
+
throw new ArtifactInputError(graphIssuesMessage(issues));
|
|
626
626
|
}
|
|
627
627
|
}
|
|
628
628
|
assertLimit(limit) {
|
|
@@ -7,6 +7,7 @@ import { HEARTBEAT_STALE_MS, TRANSITION_CONSENT_TTL_MS, TRANSITION_PREVIEW_TTL_M
|
|
|
7
7
|
import { isTerminalTransitionStage, transitionCommand, userTransitionTargets } from "../../shared/artifact-transitions/transitions.js";
|
|
8
8
|
import { sha256Hex } from "../utils/hash.js";
|
|
9
9
|
import { ArtifactConflictError, ArtifactInputError, artifactLeaseBusyError, isArtifactBusyError, isArtifactLeaseBusyError, ArtifactNotFoundError } from "../errors/artifacts.js";
|
|
10
|
+
import { graphIssuesMessage } from "./artifact-graph-validation.js";
|
|
10
11
|
// Reconcile cadence; other processes cannot call this process, so it polls.
|
|
11
12
|
const RECONCILE_INTERVAL_MS = 1000;
|
|
12
13
|
// Memory bound for unconfirmed previews.
|
|
@@ -174,7 +175,7 @@ export class ArtifactTransitionService {
|
|
|
174
175
|
}
|
|
175
176
|
if (!preview.view.canConfirm) {
|
|
176
177
|
throw new ArtifactConflictError('The transition is blocked: ' +
|
|
177
|
-
preview.view.blockers
|
|
178
|
+
graphIssuesMessage(preview.view.blockers));
|
|
178
179
|
}
|
|
179
180
|
const presetState = this.lifecycle.jobs.presetState(input.presetId);
|
|
180
181
|
if (presetState !== 'ok') {
|