@bongos/core 1.19.1081 → 1.20.1
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/.bongos-core.json +526 -191
- package/.claude/skills/planning-session/SKILL.md +6 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +32 -0
- package/clients/bongos-client/index.cjs +32 -0
- package/clients/bongos-client/index.d.ts +51 -6
- package/clients/bongos-client/index.mjs +32 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
- package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
- package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
- package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
- package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
- package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
- package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +851 -19
- package/docs/api-reference.md +33 -9
- package/docs/architecture.md +6 -2
- package/docs/copy-inventory.md +620 -553
- package/docs/copy-registry.json +1467 -820
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +6 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-inventory.json +38 -4
- package/docs/page-readings.json +1674 -1566
- package/migrations/core_260_goals_working_area.sql +63 -0
- package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
- package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
- package/modules/agents/lib/validate.js +43 -0
- package/modules/autonomy/gauge.js +38 -2
- package/modules/grading/grader-subagent.js +37 -2
- package/modules/grading/grader-workers/reader.js +65 -5
- package/modules/hall-ui/public/approval-queue.css +33 -7
- package/modules/hall-ui/public/approval-queue.js +9 -2
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/brand-holes.js +121 -0
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +6 -1
- package/modules/hall-ui/public/deploy.js +22 -8
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/dom-utils.js +20 -0
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/idea.js +28 -0
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/ideas.js +19 -3
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile-nudge.js +21 -4
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +19 -30
- package/modules/hall-ui/public/settings.js +24 -177
- package/modules/hall-ui/public/settings.states.json +1 -1
- package/modules/hall-ui/public/shell.js +4 -2
- package/modules/hall-ui/public/studio.css +14 -8
- package/modules/hall-ui/public/studio.html +5 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +6 -3
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
- package/modules/hall-ui/public/tweak-editor.css +142 -11
- package/modules/hall-ui/public/tweak-editor.html +25 -9
- package/modules/hall-ui/public/tweak-editor.js +243 -10
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/approval-queue.md +6 -0
- package/modules/hall-ui/records/tweak-editor.md +6 -0
- package/modules/ideas/ratify.js +36 -8
- package/modules/ideas/routes/ratify-goal.js +6 -0
- package/modules/lifecycle/db-analytics.js +79 -10
- package/modules/lifecycle/db-claims.js +48 -1
- package/modules/lifecycle/db-deps-criteria.js +94 -40
- package/modules/lifecycle/db-goals.js +4 -1
- package/modules/lifecycle/db-grade.js +9 -1
- package/modules/lifecycle/db-relevance-flags.js +183 -0
- package/modules/lifecycle/db-versions.js +4 -3
- package/modules/lifecycle/db.js +19 -0
- package/modules/lifecycle/done-when.js +5 -3
- package/modules/lifecycle/est-advisory.js +31 -4
- package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
- package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
- package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/routes/claims.js +8 -1
- package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
- package/modules/lifecycle/routes/goals.js +1 -1
- package/modules/lifecycle/routes/tasks.js +27 -24
- package/modules/lifecycle/routes/visuals.js +9 -0
- package/modules/lifecycle/ship-preflight.js +7 -11
- package/modules/lifecycle/task-visuals.js +109 -5
- package/modules/npm-release/module.json +2 -1
- package/modules/npm-release/preview/commands.js +81 -0
- package/modules/npm-release/preview/divert.js +95 -0
- package/modules/npm-release/preview/env.js +78 -0
- package/modules/npm-release/preview/proxy.js +107 -0
- package/modules/npm-release/preview/runtime.js +83 -0
- package/modules/npm-release/preview/supervisor.js +175 -0
- package/modules/npm-release/public/work.js +65 -1
- package/modules/npm-release/routes/preview.js +133 -0
- package/modules/provisioning/app-status.js +83 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/pollers/app-liveness.js +110 -0
- package/modules/provisioning/render-lookup.js +68 -0
- package/modules/provisioning/render-standup.js +44 -1
- package/modules/provisioning/routes/render-standup.js +81 -6
- package/modules/provisioning/starter-bundles.js +4 -1
- package/modules/public-landing/public/account-private.states.json +14 -0
- package/modules/public-landing/public/account.html +513 -0
- package/modules/public-landing/public/account.probes.json +37 -0
- package/modules/public-landing/public/account.states.json +16 -0
- package/modules/public-landing/public/index.html +5 -2
- package/modules/public-landing/public/projects.html +661 -18
- package/modules/public-landing/public/projects.states.json +8 -1
- package/modules/render-deploy/deploys.js +203 -0
- package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
- package/modules/render-deploy/module.json +25 -0
- package/modules/render-deploy/public/deploy.css +10 -0
- package/modules/render-deploy/public/deploy.js +249 -0
- package/modules/render-deploy/render.js +56 -0
- package/modules/render-deploy/routes/act.js +88 -0
- package/modules/render-deploy/routes/app.js +65 -0
- package/modules/render-deploy/routes/history.js +38 -0
- package/modules/specialities/db.js +27 -8
- package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
- package/modules/specialities/routes/specialities.js +51 -3
- package/modules/specialities/skills.js +94 -0
- package/modules/specialities/specialities.js +56 -9
- package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
- package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
- package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
- package/modules/ui-design/kit/lib.js +3 -1
- package/modules/ui-design/kit/serve.js +96 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +163 -0
- package/scripts/gds/agents-sync.js +13 -3
- package/scripts/gds/autobongos-run.js +80 -2
- package/scripts/gds/autobongos-service.cmd +12 -0
- package/scripts/gds/copy-apply.js +14 -0
- package/scripts/gds/dev-box-guard.js +3 -2
- package/scripts/gds/fitness.js +9 -0
- package/scripts/gds/grade-correlation-audit.js +42 -12
- package/scripts/gds/grade-replay.js +1 -1
- package/scripts/gds/provision-core-upgrade.js +38 -7
- package/scripts/gds/provision-pin-key.js +176 -0
- package/scripts/gds/provision-render.js +62 -31
- package/scripts/gds/provision-teardown.js +7 -2
- package/scripts/gds/provision-units.js +31 -0
- package/scripts/gds/provision.js +10 -10
- package/scripts/gds/publish-manifest.js +1 -0
- package/scripts/gds/render-api.js +19 -0
- package/scripts/gds/render-payload.js +39 -65
- package/scripts/gds/run-unit-tests.js +10 -0
- package/scripts/gds/ship-finish.js +19 -20
- package/scripts/gds/smoke-dependencies.sh +37 -8
- package/scripts/gds/status.js +12 -4
- package/scripts/gds/upgrade.js +2 -2
- package/scripts/public-mirror-export.js +7 -1
- package/src/bongos/module-scope-map.js +13 -0
- package/src/bongos/serve-internal.js +3 -1
- package/src/module-api.js +10 -1
- package/src/platform-server.js +39 -0
- package/tests/account_privacy_flags.mjs +10 -6
- package/tests/agents_authoring.mjs +15 -1
- package/tests/agents_sync.mjs +106 -5
- package/tests/agents_validate.mjs +45 -0
- package/tests/api_path_404.mjs +6 -0
- package/tests/autobongos_cadence.mjs +7 -0
- package/tests/autobongos_loop.mjs +136 -1
- package/tests/autonomy_gauge.mjs +62 -0
- package/tests/blocker_hall_live_proof.mjs +275 -0
- package/tests/claim_gate_ci_unblock.mjs +116 -0
- package/tests/claim_gate_rebase.mjs +15 -3
- package/tests/collab_page.mjs +10 -0
- package/tests/conductor_main_e2e.mjs +97 -0
- package/tests/core_262_drop_box_blocked_db.mjs +143 -0
- package/tests/core_upgrade_runner.mjs +1 -0
- package/tests/dependency_writes_atomic.mjs +191 -0
- package/tests/deploy_page_projects.mjs +2 -1
- package/tests/effective_visibility_predicate.mjs +15 -0
- package/tests/est_advisory.mjs +27 -0
- package/tests/goal_map_page.mjs +26 -0
- package/tests/goal_task_relevance.mjs +518 -0
- package/tests/goal_working_area.mjs +146 -0
- package/tests/grade_attempts.mjs +61 -0
- package/tests/grade_attribution.mjs +23 -0
- package/tests/grade_correlation_audit.mjs +54 -1
- package/tests/grader_reader_lens.mjs +55 -2
- package/tests/grader_root_outage.mjs +6 -1
- package/tests/grader_subagent_tools_arg.mjs +102 -0
- package/tests/hall_approval_queue.mjs +54 -2
- package/tests/hall_settings_world.mjs +3 -1
- package/tests/hall_tweak_editor.mjs +316 -11
- package/tests/hub_account_page.mjs +394 -0
- package/tests/idea_detail_page.mjs +64 -0
- package/tests/idea_goal_ratification.mjs +64 -0
- package/tests/idea_spark_hall.mjs +60 -9
- package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
- package/tests/landing_page.mjs +6 -3
- package/tests/module-scope-map.mjs +31 -8
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/no_phantom_mirror_workflow.mjs +24 -0
- package/tests/npm_release_preview_commands.mjs +89 -0
- package/tests/npm_release_preview_divert.mjs +164 -0
- package/tests/npm_release_preview_env.mjs +100 -0
- package/tests/npm_release_preview_proxy.mjs +162 -0
- package/tests/npm_release_preview_routes.mjs +208 -0
- package/tests/npm_release_preview_supervisor.mjs +203 -0
- package/tests/pin_write_key.mjs +383 -0
- package/tests/planning_session_skill.mjs +27 -0
- package/tests/platform_boot.mjs +71 -0
- package/tests/profile_nudge_links.mjs +86 -0
- package/tests/profile_ui_cross_project.mjs +1 -1
- package/tests/projects_hub.mjs +2 -2
- package/tests/projects_hub_app_status.mjs +249 -0
- package/tests/projects_hub_app_step.mjs +282 -52
- package/tests/projects_hub_render_connect.mjs +295 -0
- package/tests/provision_render.mjs +121 -28
- package/tests/provision_settings_apply.mjs +62 -0
- package/tests/provisioning_app_status.mjs +204 -0
- package/tests/provisioning_render_route.mjs +126 -6
- package/tests/render_check.mjs +2 -1
- package/tests/render_deploy.mjs +495 -0
- package/tests/ship_preflight.mjs +61 -8
- package/tests/smoke_dependencies_witness.mjs +46 -0
- package/tests/speciality_skills.mjs +214 -0
- package/tests/studio_room_height.mjs +70 -0
- package/tests/task_detail_includes.mjs +10 -0
- package/tests/task_visuals_instance_root.mjs +150 -0
- package/tests/tweak_batch_apply.mjs +21 -0
- package/tests/ui_design_kit.mjs +36 -4
- package/tests/upgrade.mjs +1 -1
- package/tests/upgrade_persist_pin.mjs +24 -0
package/modules/ideas/ratify.js
CHANGED
|
@@ -261,6 +261,18 @@ async function ratifyCriterionProposal(ideaId, ratifierId, { notesMd = '' } = {}
|
|
|
261
261
|
// refused with TEMPLATE_HAS_NO_GOAL_SHAPE, naming the vintage; its author can
|
|
262
262
|
// resubmit against the current questions, which is what the resubmit path is for.
|
|
263
263
|
//
|
|
264
|
+
// THE RATIFIER MAY WRITE IT INSTEAD (task 1004226, owner decision 2026-09-29).
|
|
265
|
+
// The board's first ten passed Full Ideas were all base-v1, so the refusal above
|
|
266
|
+
// stranded every one of them. The rule that makes it a refusal still holds:
|
|
267
|
+
// nothing here DERIVES a criterion from a v1 answer. What changed is that the
|
|
268
|
+
// person ratifying may state one in their own words (`success_criterion` on the
|
|
269
|
+
// request). The goal description then carries all the author's answers as
|
|
270
|
+
// written plus a line saying the done-when was written by the ratifier, so a
|
|
271
|
+
// reader never mistakes it for the author's answer; the ratifier's identity is on
|
|
272
|
+
// the audit row. It is ONLY for a template this table does not map: where the
|
|
273
|
+
// author answered the success question, their answer is the criterion and a
|
|
274
|
+
// ratifier's is refused rather than substituted for it.
|
|
275
|
+
//
|
|
264
276
|
// ADDING A VINTAGE MEANS ADDING AN ENTRY HERE. Nothing falls back — an unmapped
|
|
265
277
|
// template refuses, rather than guessing at a key that happens to exist.
|
|
266
278
|
const GOAL_SHAPE_BY_TEMPLATE = Object.freeze({
|
|
@@ -286,17 +298,24 @@ const GOAL_SHAPE_BY_TEMPLATE = Object.freeze({
|
|
|
286
298
|
// answers in whatever order Postgres chose (templates.js' own header says so).
|
|
287
299
|
// LABELS come from the declaration too, so a v1 row would render under v1
|
|
288
300
|
// labels if a v1 shape were ever declared.
|
|
289
|
-
function composeGoalFromFullIdea({ ideaId, templateKey, fieldValues } = {}) {
|
|
301
|
+
function composeGoalFromFullIdea({ ideaId, templateKey, fieldValues, ratifierCriterionMd = null } = {}) {
|
|
290
302
|
const template = templateFor(templateKey);
|
|
291
303
|
if (!template) return { ok: false, reason: 'unknown_template', template_key: templateKey ?? null };
|
|
292
304
|
const shape = Object.hasOwn(GOAL_SHAPE_BY_TEMPLATE, templateKey) ? GOAL_SHAPE_BY_TEMPLATE[templateKey] : null;
|
|
293
|
-
|
|
305
|
+
// A blank ratifier criterion is no criterion: it must not turn a refusal into
|
|
306
|
+
// a goal whose only gate is whitespace.
|
|
307
|
+
const ratifierMd = typeof ratifierCriterionMd === 'string' && ratifierCriterionMd.trim() !== ''
|
|
308
|
+
? ratifierCriterionMd.trim() : null;
|
|
309
|
+
if (shape && ratifierMd) {
|
|
310
|
+
return { ok: false, reason: 'template_supplies_done_when', template_key: templateKey, field: shape.doneWhenField };
|
|
311
|
+
}
|
|
312
|
+
if (!shape && !ratifierMd) return { ok: false, reason: 'template_has_no_goal_shape', template_key: templateKey };
|
|
294
313
|
if (!fieldValues || typeof fieldValues !== 'object' || Array.isArray(fieldValues)) {
|
|
295
314
|
return { ok: false, reason: 'field_values_missing' };
|
|
296
315
|
}
|
|
297
316
|
|
|
298
|
-
const doneWhen = fieldValues[shape.doneWhenField];
|
|
299
|
-
if (typeof doneWhen !== 'string' || doneWhen.trim() === '') {
|
|
317
|
+
const doneWhen = shape ? fieldValues[shape.doneWhenField] : ratifierMd;
|
|
318
|
+
if (shape && (typeof doneWhen !== 'string' || doneWhen.trim() === '')) {
|
|
300
319
|
// A Full Idea cannot normally reach here with a blank required field —
|
|
301
320
|
// validateFieldValues refuses that at capture. It is checked anyway because
|
|
302
321
|
// the alternative is a goal whose only criterion is an empty string, which
|
|
@@ -322,11 +341,17 @@ function composeGoalFromFullIdea({ ideaId, templateKey, fieldValues } = {}) {
|
|
|
322
341
|
// opening the goal learns, without asking anyone, that its scope is somebody's
|
|
323
342
|
// thought and where to go and read it whole.
|
|
324
343
|
const origin = `> Ratified from Full Idea [#${ideaId}](/idea/${ideaId}) — its five answers, as written.`;
|
|
344
|
+
// Said in the prose, because the criterion is not one of the answers above and
|
|
345
|
+
// a reader must not take it for the author's (task 1004226).
|
|
346
|
+
const byRatifier = shape ? [] : [
|
|
347
|
+
`> Its done-when criterion was written by the ratifier at ratification: this idea's questions (${templateKey}) did not ask what success looks like.`,
|
|
348
|
+
];
|
|
325
349
|
return {
|
|
326
350
|
ok: true,
|
|
327
|
-
description: [origin, ...sections].join('\n\n'),
|
|
351
|
+
description: [origin, ...byRatifier, ...sections].join('\n\n'),
|
|
328
352
|
criterionMd: doneWhen.trim(),
|
|
329
|
-
doneWhenField: shape.doneWhenField,
|
|
353
|
+
doneWhenField: shape ? shape.doneWhenField : null,
|
|
354
|
+
doneWhenFrom: shape ? shape.doneWhenField : 'ratifier',
|
|
330
355
|
};
|
|
331
356
|
}
|
|
332
357
|
|
|
@@ -353,7 +378,7 @@ function composeGoalFromFullIdea({ ideaId, templateKey, fieldValues } = {}) {
|
|
|
353
378
|
// recommendation a decision, which is the one thing it is not.
|
|
354
379
|
async function ratifyFullIdeaAsGoal(
|
|
355
380
|
ideaId, ratifierId,
|
|
356
|
-
{ targetVersion, scopeModules = [], admitReason = null, actorRank = null, notesMd = '' } = {}
|
|
381
|
+
{ targetVersion, scopeModules = [], admitReason = null, actorRank = null, notesMd = '', ratifierCriterionMd = null } = {}
|
|
357
382
|
) {
|
|
358
383
|
const id = Number(ideaId);
|
|
359
384
|
if (!Number.isFinite(id)) throw { code: 'BAD_ID' };
|
|
@@ -409,6 +434,7 @@ async function ratifyFullIdeaAsGoal(
|
|
|
409
434
|
// c. Compose. Pure, and its refusals are the caller's 400s.
|
|
410
435
|
const composed = composeGoalFromFullIdea({
|
|
411
436
|
ideaId: idea.id, templateKey: idea.template_key, fieldValues: idea.field_values,
|
|
437
|
+
ratifierCriterionMd,
|
|
412
438
|
});
|
|
413
439
|
if (!composed.ok) throw { code: 'CANNOT_COMPOSE_GOAL', reason: composed.reason, ...composed };
|
|
414
440
|
|
|
@@ -476,7 +502,9 @@ async function ratifyFullIdeaAsGoal(
|
|
|
476
502
|
// Reported back so a caller can see what the author had recommended and
|
|
477
503
|
// what the board did with it, without a second read. NULL stays NULL.
|
|
478
504
|
recommendation_at_filing: idea.recommendation ?? null,
|
|
479
|
-
|
|
505
|
+
// 'ratifier' when the person ratifying wrote it (task 1004226), else the
|
|
506
|
+
// template field the author answered.
|
|
507
|
+
done_when_from: composed.doneWhenFrom,
|
|
480
508
|
proposer_id: idea.captured_by,
|
|
481
509
|
};
|
|
482
510
|
} catch (err) {
|
|
@@ -110,6 +110,11 @@ module.exports = function buildRatifyGoalRouter() {
|
|
|
110
110
|
// 'ok' is not a reason.
|
|
111
111
|
admit_reason: { type: 'string', minLength: 8, maxLength: 500 },
|
|
112
112
|
notes_md: { type: 'string', maxLength: LIMITS.NOTES },
|
|
113
|
+
// task 1004226: the success criterion, in the RATIFIER's words, for a
|
|
114
|
+
// Full Idea whose questions never asked what success looks like
|
|
115
|
+
// (base-v1). Refused for a template that did ask, so the author's own
|
|
116
|
+
// answer is never replaced. Absent or blank means none was written.
|
|
117
|
+
success_criterion: { type: 'string', maxLength: 2000 },
|
|
113
118
|
})) return;
|
|
114
119
|
const id = parseId(req, res, { code: 'bad_id' });
|
|
115
120
|
if (id === null) return;
|
|
@@ -122,6 +127,7 @@ module.exports = function buildRatifyGoalRouter() {
|
|
|
122
127
|
// (ADR 0016) — the port denies when it is absent.
|
|
123
128
|
actorRank: req.builder.rank,
|
|
124
129
|
notesMd: typeof req.body.notes_md === 'string' ? req.body.notes_md : '',
|
|
130
|
+
ratifierCriterionMd: typeof req.body.success_criterion === 'string' ? req.body.success_criterion : null,
|
|
125
131
|
});
|
|
126
132
|
res.status(201).json({ ok: true, ...result });
|
|
127
133
|
} catch (err) {
|
|
@@ -579,18 +579,60 @@ async function ecosystemAnalytics(days = 90, kind = null) {
|
|
|
579
579
|
// writes actually queryable through the API, not just via psql
|
|
580
580
|
// (ADR 0033 §1 / F4 finding).
|
|
581
581
|
|
|
582
|
-
|
|
582
|
+
// workerVerdicts — one grade's per-worker signals slimmed to what the
|
|
583
|
+
// correlation audit reads (task 1004292): kind, verdict, the error CODE and the
|
|
584
|
+
// severities of the worker's issues. NO prose: no summary, file or stderr, which
|
|
585
|
+
// task_grades keeps for the latest grade only. Pure; returns null when there is
|
|
586
|
+
// nothing to keep (a non-panel grade), so the column stays NULL.
|
|
587
|
+
const VERDICT_WORKER_CAP = 12; // workers kept per attempt (a panel is ~4-6 plus pseudo-judges)
|
|
588
|
+
const VERDICT_SEVERITY_CAP = 50; // severities kept per worker
|
|
589
|
+
const VERDICT_KIND_MAX = 40;
|
|
590
|
+
const VERDICT_VERDICT_MAX = 20;
|
|
591
|
+
const VERDICT_ERROR_MAX = 80;
|
|
592
|
+
const VERDICT_SEVERITY_MAX = 20;
|
|
593
|
+
const VERDICT_ROUNDS_MAX = 50; // rounds served per task (the most recent)
|
|
594
|
+
function workerVerdicts(workers) {
|
|
595
|
+
if (!Array.isArray(workers)) return null;
|
|
596
|
+
const out = [];
|
|
597
|
+
for (const w of workers) {
|
|
598
|
+
if (!w || typeof w !== 'object' || typeof w.kind !== 'string') continue;
|
|
599
|
+
out.push({
|
|
600
|
+
kind: w.kind.slice(0, VERDICT_KIND_MAX),
|
|
601
|
+
verdict: typeof w.verdict === 'string' ? w.verdict.slice(0, VERDICT_VERDICT_MAX) : null,
|
|
602
|
+
error: w.error == null ? null : String(w.error).slice(0, VERDICT_ERROR_MAX),
|
|
603
|
+
severities: (Array.isArray(w.issues) ? w.issues : [])
|
|
604
|
+
.map((i) => (i && typeof i.severity === 'string' ? i.severity.slice(0, VERDICT_SEVERITY_MAX) : null))
|
|
605
|
+
.filter(Boolean)
|
|
606
|
+
.slice(0, VERDICT_SEVERITY_CAP),
|
|
607
|
+
});
|
|
608
|
+
if (out.length >= VERDICT_WORKER_CAP) break;
|
|
609
|
+
}
|
|
610
|
+
return out.length ? out : null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function logGradeAttempt({ taskId, builderId, passed, score, workers }, queryFn) {
|
|
583
614
|
const q = queryFn || ((text, params) => pool.query(text, params));
|
|
615
|
+
const base = [
|
|
616
|
+
Number(taskId),
|
|
617
|
+
builderId == null ? null : Number(builderId),
|
|
618
|
+
passed === true,
|
|
619
|
+
score == null ? null : Number(score),
|
|
620
|
+
];
|
|
621
|
+
const verdicts = workerVerdicts(workers);
|
|
622
|
+
// task 1004292: the per-worker verdicts ride the same row, but a failure to
|
|
623
|
+
// write them (the column not yet migrated, a serialisation error) must never
|
|
624
|
+
// cost the row itself, so the plain insert is the fallback.
|
|
625
|
+
if (verdicts) {
|
|
626
|
+
try {
|
|
627
|
+
await q(
|
|
628
|
+
`INSERT INTO grade_attempts (task_id, builder_id, passed, score, worker_verdicts) VALUES ($1, $2, $3, $4, $5::jsonb)`,
|
|
629
|
+
[...base, JSON.stringify(verdicts)]
|
|
630
|
+
);
|
|
631
|
+
return true;
|
|
632
|
+
} catch (_err) { /* fall through to the plain insert below, which logs if it fails too */ }
|
|
633
|
+
}
|
|
584
634
|
try {
|
|
585
|
-
await q(
|
|
586
|
-
`INSERT INTO grade_attempts (task_id, builder_id, passed, score) VALUES ($1, $2, $3, $4)`,
|
|
587
|
-
[
|
|
588
|
-
Number(taskId),
|
|
589
|
-
builderId == null ? null : Number(builderId),
|
|
590
|
-
passed === true,
|
|
591
|
-
score == null ? null : Number(score),
|
|
592
|
-
]
|
|
593
|
-
);
|
|
635
|
+
await q(`INSERT INTO grade_attempts (task_id, builder_id, passed, score) VALUES ($1, $2, $3, $4)`, base);
|
|
594
636
|
return true;
|
|
595
637
|
} catch (err) {
|
|
596
638
|
console.error('[gds] logGradeAttempt (non-blocking):', err && err.message ? err.message : err);
|
|
@@ -598,6 +640,30 @@ async function logGradeAttempt({ taskId, builderId, passed, score }, queryFn) {
|
|
|
598
640
|
}
|
|
599
641
|
}
|
|
600
642
|
|
|
643
|
+
// gradeAttemptVerdicts — every recorded round of a task that kept its per-worker
|
|
644
|
+
// verdicts, oldest first: [{ attempted_at, passed, workers }] (task 1004292).
|
|
645
|
+
// Rounds made before the column existed are simply absent. Never throws: a read
|
|
646
|
+
// failure (column not migrated yet) is an empty list, and the audit falls back
|
|
647
|
+
// to the task's final grade.
|
|
648
|
+
async function gradeAttemptVerdicts(taskId, queryFn) {
|
|
649
|
+
const q = queryFn || ((text, params) => pool.query(text, params));
|
|
650
|
+
try {
|
|
651
|
+
const { rows } = await q(
|
|
652
|
+
`SELECT attempted_at, passed, workers FROM (
|
|
653
|
+
SELECT id, attempted_at, passed, worker_verdicts AS workers
|
|
654
|
+
FROM grade_attempts
|
|
655
|
+
WHERE task_id = $1 AND worker_verdicts IS NOT NULL
|
|
656
|
+
ORDER BY attempted_at DESC, id DESC
|
|
657
|
+
LIMIT $2) recent
|
|
658
|
+
ORDER BY attempted_at ASC, id ASC`,
|
|
659
|
+
[Number(taskId), VERDICT_ROUNDS_MAX]
|
|
660
|
+
);
|
|
661
|
+
return rows;
|
|
662
|
+
} catch (_err) {
|
|
663
|
+
return []; // best-effort read: an unmigrated column means "no earlier rounds", not an error
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
601
667
|
// countGradeAttempts — how many times a task has been graded (re-grade thrash).
|
|
602
668
|
|
|
603
669
|
async function countGradeAttempts(taskId, queryFn) {
|
|
@@ -777,6 +843,9 @@ module.exports = {
|
|
|
777
843
|
gradeByBuilder,
|
|
778
844
|
gradeTrendByDay,
|
|
779
845
|
logGradeAttempt,
|
|
846
|
+
gradeAttemptVerdicts,
|
|
847
|
+
workerVerdicts,
|
|
848
|
+
VERDICT_LIMITS: { workers: VERDICT_WORKER_CAP, severities: VERDICT_SEVERITY_CAP, rounds: VERDICT_ROUNDS_MAX },
|
|
780
849
|
mergeLeaseStatus,
|
|
781
850
|
recentGradesForBuilder,
|
|
782
851
|
recentShippedTasks,
|
|
@@ -22,6 +22,11 @@ const { touchesOverlap } = api.pathMatch;
|
|
|
22
22
|
// un-testable except by counting.
|
|
23
23
|
const { CLAIM_ANY, CLAIM_NEWCOMER } = require('./claim-eligibility.js');
|
|
24
24
|
const { goalMembershipFailures } = require('./claim-feed.js');
|
|
25
|
+
// ADR 0101 Part 1 (task 1001691) — the relevance/anti-hack judge's claim-time
|
|
26
|
+
// friction gate. Same extraction shape as goalMembershipFailures: one decision,
|
|
27
|
+
// called from all three claim surfaces below, so a flagged task can't be
|
|
28
|
+
// claimed via the batch path as a bypass of the single-claim gate.
|
|
29
|
+
const { relevanceFrictionFailures } = require('./db-relevance-flags.js');
|
|
25
30
|
const { unsatisfiedDeps } = require('./db-deps-criteria.js');
|
|
26
31
|
// depRefusalRow: the ONE shape a DEPS_NOT_SHIPPED refusal carries. All three
|
|
27
32
|
// claim paths below use it — hand-building the object here is what made task
|
|
@@ -79,6 +84,21 @@ function claimUniqueViolationError(err) {
|
|
|
79
84
|
return { code: 'ALREADY_CLAIMED' };
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
// isRebaseGateExempt — the one task the owed-rebase gate below never refuses
|
|
88
|
+
// (task 1004370). When main's CI goes red for a reason outside every branch (an
|
|
89
|
+
// npm advisory tripping the audit gate, 2026-09-29), no confirmed task can land,
|
|
90
|
+
// so the gate shuts the queue of every builder holding one — including the claim
|
|
91
|
+
// on the fix that would let them land. Releasing is no exit either: a confirmed
|
|
92
|
+
// task has no open claim to release. So a P1 kind=infra task, the shape a
|
|
93
|
+
// CI-unblock is filed in, passes the gate. Everything else stays gated, and the
|
|
94
|
+
// owed rebases are still reported everywhere they were. Filing one takes
|
|
95
|
+
// task.create, which is Metic-floored (catalog.js), so the door is as narrow as
|
|
96
|
+
// the people who could already file the fix. A missing row is not exempt, so a
|
|
97
|
+
// gated builder still meets REBASE_REQUIRED first, in the order it always was.
|
|
98
|
+
function isRebaseGateExempt(task) {
|
|
99
|
+
return Boolean(task) && task.kind === 'infra' && Number(task.priority) === 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
82
102
|
async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSessionId, enforceOverlap = false }) {
|
|
83
103
|
const heldPermissions = await claimPermissionsFor(builderId); // BV1.R106 — before the tx opens
|
|
84
104
|
const client = await pool.connect();
|
|
@@ -112,7 +132,9 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
|
|
|
112
132
|
// The GATING subset, not the surfacing one: a strand the builder cannot clear
|
|
113
133
|
// stops blocking after a grace, while still being reported (task 1003294).
|
|
114
134
|
const owedRebases = await gatingRebaseTasksForBuilder(builderId, client);
|
|
115
|
-
if (owedRebases.length > 0
|
|
135
|
+
if (owedRebases.length > 0 && !isRebaseGateExempt(
|
|
136
|
+
(await client.query(`SELECT kind, priority FROM tasks WHERE id = $1`, [taskId])).rows[0],
|
|
137
|
+
)) {
|
|
116
138
|
throw {
|
|
117
139
|
code: 'REBASE_REQUIRED',
|
|
118
140
|
rebaseTasks: owedRebases.map((t) => ({ id: t.id, title: t.title, reason: t.needs_rebase_reason })),
|
|
@@ -213,6 +235,14 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
|
|
|
213
235
|
const goalRefusals = await goalMembershipFailures({ client, tasks: [task], builderId, builderRank });
|
|
214
236
|
if (goalRefusals.length > 0) throw goalRefusals[0].refusal;
|
|
215
237
|
|
|
238
|
+
// task 1001691 / ADR 0101 Part 1: the relevance judge's "raise friction"
|
|
239
|
+
// gate — a high-confidence or repeat scope-gaming flag holds the claim
|
|
240
|
+
// open until a Metic reviews it (never permanently; either decision
|
|
241
|
+
// releases it). Same ladder position as goal membership: after rank +
|
|
242
|
+
// membership, before deps.
|
|
243
|
+
const relevanceRefusals = await relevanceFrictionFailures({ client, tasks: [task] });
|
|
244
|
+
if (relevanceRefusals.length > 0) throw relevanceRefusals[0].refusal;
|
|
245
|
+
|
|
216
246
|
// The claim gate (ADR 0015, generalized in task 1516 / ADR 0086 §5): a task
|
|
217
247
|
// can't be claimed while any dependency is un-done — its OWN edges plus those
|
|
218
248
|
// inherited from its goal/criteria, where "done" = task shipped | criterion
|
|
@@ -593,6 +623,15 @@ async function claimTasksBatch({ taskIds, builderId, worktreePrefix, creatorSess
|
|
|
593
623
|
failures.push({ task_id: f.task_id, reason: code, ...rest });
|
|
594
624
|
}
|
|
595
625
|
|
|
626
|
+
// task 1001691 / ADR 0101 Part 1: the SAME extracted relevance-friction
|
|
627
|
+
// gate as claimTask — kept in sync for the same reason goal membership is
|
|
628
|
+
// (see claim-feed.js's header): the batch path owns the repo's only other
|
|
629
|
+
// INSERT INTO claims, so an inline-only gate would be bypassable via batch.
|
|
630
|
+
for (const f of await relevanceFrictionFailures({ client, tasks: taskRows })) {
|
|
631
|
+
const { code, ...rest } = f.refusal;
|
|
632
|
+
failures.push({ task_id: f.task_id, reason: code, ...rest });
|
|
633
|
+
}
|
|
634
|
+
|
|
596
635
|
// Per-task dependency check (ADR 0015, generalized task 1516). An EXTERNAL dep
|
|
597
636
|
// (not in this batch) must already be done; a batch-internal dep isn't done yet
|
|
598
637
|
// at claim time but doesn't block — the batch is worked serially. "Internal"
|
|
@@ -939,6 +978,13 @@ async function validateClaimBatch({ taskIds, builderId }) {
|
|
|
939
978
|
push(f.task_id, { reason: code, ...rest });
|
|
940
979
|
}
|
|
941
980
|
|
|
981
|
+
// task 1001691 / ADR 0101 Part 1: relevance-friction, kept in sync with
|
|
982
|
+
// claimTask + claimTasksBatch (see db-relevance-flags.js).
|
|
983
|
+
for (const f of await relevanceFrictionFailures({ client: pool, tasks: taskRows })) {
|
|
984
|
+
const { code, ...rest } = f.refusal;
|
|
985
|
+
push(f.task_id, { reason: code, ...rest });
|
|
986
|
+
}
|
|
987
|
+
|
|
942
988
|
// Dependencies — external to the batch only (a batch-internal dep is fine;
|
|
943
989
|
// the batch is worked serially, same rule as claimTasksBatch).
|
|
944
990
|
const batchIdSet = new Set(uniqIds);
|
|
@@ -1006,6 +1052,7 @@ module.exports = {
|
|
|
1006
1052
|
claimTask,
|
|
1007
1053
|
claimTasksBatch,
|
|
1008
1054
|
claimUniqueViolationError,
|
|
1055
|
+
isRebaseGateExempt,
|
|
1009
1056
|
releaseClaimsBatch,
|
|
1010
1057
|
validateClaimBatch,
|
|
1011
1058
|
};
|
|
@@ -147,28 +147,72 @@ async function findDependencyCycle(taskId, depTaskId, executor = pool) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
await
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
150
|
+
// ---- every dependency-graph WRITE is one locked transaction (task 1004191) ----
|
|
151
|
+
// The edge writers used to run their existence check, cycle check and INSERT as
|
|
152
|
+
// separate statements on the bare pool. Two findings from the task 1000379 audit
|
|
153
|
+
// followed from that, both verified real on 2026-09-29:
|
|
154
|
+
// - partial writes (63/81): POST /tasks/:id/dependencies looped addTaskDependency
|
|
155
|
+
// per id, so a cycle on the 3rd id left the 1st and 2nd committed;
|
|
156
|
+
// - TOCTOU (75): the cycle walk and the INSERT ran on different connections, so
|
|
157
|
+
// concurrent A->B and B->A could both pass the walk and commit a cycle.
|
|
158
|
+
// Now each writer opens ONE transaction, takes ONE advisory lock that serializes
|
|
159
|
+
// every graph write (the graph is small and written rarely, by Metic+ only), and
|
|
160
|
+
// runs existence + cycle + insert on that client. Under READ COMMITTED each
|
|
161
|
+
// statement after the lock sees every write that committed before the lock was
|
|
162
|
+
// granted, so the walk always sees the other writer's edge.
|
|
163
|
+
const DEP_GRAPH_LOCK = 'dependencies/graph-write';
|
|
164
|
+
|
|
165
|
+
async function lockDependencyGraph(client) {
|
|
166
|
+
await client.query('SELECT pg_advisory_xact_lock(hashtext($1))', [DEP_GRAPH_LOCK]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Coerce + validate a list of ids. Throws BAD_ID (a 400) instead of letting a
|
|
170
|
+
// non-numeric id reach Postgres as a bigint cast error (a 500 — finding 64).
|
|
171
|
+
function toDepIds(list) {
|
|
172
|
+
const ids = list.map(Number);
|
|
173
|
+
if (ids.some((n) => !Number.isSafeInteger(n) || n <= 0)) throw { code: 'BAD_ID' };
|
|
174
|
+
return [...new Set(ids)];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// There is no FK on dependencies (the target table varies by kind), so an unknown
|
|
178
|
+
// id would be written as an orphan edge that blocks its task forever (fail-closed).
|
|
179
|
+
// Refuse it up front and name what was missing.
|
|
180
|
+
async function assertTasksExist(client, ids) {
|
|
181
|
+
const { rows } = await client.query(`SELECT id FROM tasks WHERE id = ANY($1::bigint[])`, [ids]);
|
|
182
|
+
const found = new Set(rows.map((r) => Number(r.id)));
|
|
183
|
+
const missing = ids.filter((id) => !found.has(id));
|
|
184
|
+
if (missing.length > 0) throw { code: 'TASK_NOT_FOUND', missing };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const txOpts = (opts) => (opts && opts.pool ? { pool: opts.pool } : undefined);
|
|
188
|
+
|
|
189
|
+
// Add task->task edges from taskId to every id in depTaskIds, all or nothing.
|
|
190
|
+
// Throws BAD_ID, SELF_DEP, TASK_NOT_FOUND ({missing}) or CYCLE ({path}); on any
|
|
191
|
+
// throw nothing is written. Returns the unique dep ids. `opts.pool` is a test seam.
|
|
192
|
+
async function addTaskDependencies(taskId, depTaskIds, opts) {
|
|
193
|
+
const [id] = toDepIds([taskId]);
|
|
194
|
+
const deps = toDepIds(depTaskIds);
|
|
195
|
+
if (deps.includes(id)) throw { code: 'SELF_DEP' };
|
|
196
|
+
return withTx(async (client) => {
|
|
197
|
+
await lockDependencyGraph(client);
|
|
198
|
+
await assertTasksExist(client, [id, ...deps]);
|
|
199
|
+
for (const dep of deps) {
|
|
200
|
+
const cyclePath = await findDependencyCycle(id, dep, client);
|
|
201
|
+
if (cyclePath) throw { code: 'CYCLE', path: cyclePath };
|
|
202
|
+
await client.query(
|
|
203
|
+
`INSERT INTO dependencies (from_kind, from_id, to_kind, to_id)
|
|
204
|
+
VALUES ('task', $1, 'task', $2)
|
|
205
|
+
ON CONFLICT DO NOTHING`,
|
|
206
|
+
[id, dep]
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return deps;
|
|
210
|
+
}, txOpts(opts));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Single-edge form (routes/goals.js). Same transaction; historical return shape.
|
|
214
|
+
async function addTaskDependency(taskId, depTaskId, opts) {
|
|
215
|
+
await addTaskDependencies(taskId, [depTaskId], opts);
|
|
172
216
|
return { task_id: taskId, depends_on_task_id: depTaskId };
|
|
173
217
|
}
|
|
174
218
|
|
|
@@ -187,34 +231,39 @@ async function removeTaskDependency(taskId, depTaskId) {
|
|
|
187
231
|
// Validates no cycles among the new set in one transaction. Order of the
|
|
188
232
|
// list doesn't matter.
|
|
189
233
|
|
|
190
|
-
async function setTaskDependencies(taskId, depTaskIds) {
|
|
191
|
-
const
|
|
192
|
-
|
|
234
|
+
async function setTaskDependencies(taskId, depTaskIds, opts) {
|
|
235
|
+
const [id] = toDepIds([taskId]);
|
|
236
|
+
const unique = toDepIds(depTaskIds);
|
|
237
|
+
if (unique.includes(id)) {
|
|
193
238
|
throw { code: 'SELF_DEP' };
|
|
194
239
|
}
|
|
195
240
|
return withTx(async (client) => {
|
|
241
|
+
// task 1004191: serialize against every other graph write (finding 75), and
|
|
242
|
+
// 404 an unknown task or dep id instead of writing orphan edges (finding 64).
|
|
243
|
+
await lockDependencyGraph(client);
|
|
244
|
+
await assertTasksExist(client, [id, ...unique]);
|
|
196
245
|
// Replace only this task's task->task edges; leave any task->goal/criterion
|
|
197
246
|
// edges (created via the generic /dependencies endpoint) untouched.
|
|
198
247
|
await client.query(
|
|
199
248
|
`DELETE FROM dependencies WHERE from_kind = 'task' AND from_id = $1 AND to_kind = 'task'`,
|
|
200
|
-
[
|
|
249
|
+
[id]
|
|
201
250
|
);
|
|
202
251
|
for (const dep of unique) {
|
|
203
252
|
// Re-check cycle for each insert (using the same txn connection so the
|
|
204
253
|
// previous inserts in this txn are visible). Reuses findDependencyCycle
|
|
205
254
|
// with the txn client as executor — task 1087, C3.
|
|
206
|
-
const cyclePath = await findDependencyCycle(
|
|
255
|
+
const cyclePath = await findDependencyCycle(id, dep, client);
|
|
207
256
|
if (cyclePath) {
|
|
208
257
|
throw { code: 'CYCLE', path: cyclePath };
|
|
209
258
|
}
|
|
210
259
|
await client.query(
|
|
211
260
|
`INSERT INTO dependencies (from_kind, from_id, to_kind, to_id)
|
|
212
261
|
VALUES ('task', $1, 'task', $2)`,
|
|
213
|
-
[
|
|
262
|
+
[id, dep]
|
|
214
263
|
);
|
|
215
264
|
}
|
|
216
265
|
return unique;
|
|
217
|
-
});
|
|
266
|
+
}, txOpts(opts));
|
|
218
267
|
}
|
|
219
268
|
|
|
220
269
|
// ---------------------------------------------------------------------------
|
|
@@ -240,19 +289,23 @@ async function dependencyNodeExists(kind, id, executor = pool) {
|
|
|
240
289
|
// Add a polymorphic edge "from is blocked until to is done". Throws BAD_KIND,
|
|
241
290
|
// SELF_DEP, NODE_NOT_FOUND, or CYCLE (with the offending kind:id path).
|
|
242
291
|
|
|
243
|
-
async function addDependency(fromKind, fromId, toKind, toId) {
|
|
292
|
+
async function addDependency(fromKind, fromId, toKind, toId, opts) {
|
|
244
293
|
if (!DEP_KINDS.includes(fromKind) || !DEP_KINDS.includes(toKind)) throw { code: 'BAD_KIND' };
|
|
245
294
|
if (fromKind === toKind && Number(fromId) === Number(toId)) throw { code: 'SELF_DEP' };
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
295
|
+
// task 1004191 (finding 75): one locked transaction, same as the task writers.
|
|
296
|
+
await withTx(async (client) => {
|
|
297
|
+
await lockDependencyGraph(client);
|
|
298
|
+
if (!(await dependencyNodeExists(fromKind, fromId, client)) || !(await dependencyNodeExists(toKind, toId, client))) {
|
|
299
|
+
throw { code: 'NODE_NOT_FOUND' };
|
|
300
|
+
}
|
|
301
|
+
const cyclePath = await findCycle(fromKind, fromId, toKind, toId, client);
|
|
302
|
+
if (cyclePath) throw { code: 'CYCLE', path: cyclePath };
|
|
303
|
+
await client.query(
|
|
304
|
+
`INSERT INTO dependencies (from_kind, from_id, to_kind, to_id)
|
|
305
|
+
VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING`,
|
|
306
|
+
[fromKind, fromId, toKind, toId]
|
|
307
|
+
);
|
|
308
|
+
}, txOpts(opts));
|
|
256
309
|
return { from_kind: fromKind, from_id: Number(fromId), to_kind: toKind, to_id: Number(toId) };
|
|
257
310
|
}
|
|
258
311
|
|
|
@@ -488,6 +541,7 @@ async function unsatisfiedDeps(taskId, client = pool) {
|
|
|
488
541
|
module.exports = {
|
|
489
542
|
addDependency,
|
|
490
543
|
addTaskCriterion,
|
|
544
|
+
addTaskDependencies,
|
|
491
545
|
addTaskDependency,
|
|
492
546
|
attachTaskDepSummaries,
|
|
493
547
|
countTasksWithoutCriterionLink,
|
|
@@ -43,8 +43,11 @@ const { autoCloseVersionsForGoals } = require('./version-autoclose.js');
|
|
|
43
43
|
// the close count's exemption, and the hall, which should not render the one goal
|
|
44
44
|
// nobody chose as though a person had scoped it. Additive: every consumer selects
|
|
45
45
|
// this list, so nothing had to be found and changed.
|
|
46
|
+
// `working_area` (task 1004057, migration core_260) is the ADR 0264 area number
|
|
47
|
+
// as data, NULL for every other goal. It is NOT sort_order and NOT the id — both
|
|
48
|
+
// look like it on a payload and neither is.
|
|
46
49
|
const GOAL_COLS =
|
|
47
|
-
'id, version_id, title, subtitle, description, scope_modules, status, visibility, accepting_requests, created_by, succeeded_by_goal_id, sort_order, category_id, is_maintenance, created_at, updated_at';
|
|
50
|
+
'id, version_id, title, subtitle, description, scope_modules, status, visibility, accepting_requests, created_by, succeeded_by_goal_id, sort_order, category_id, is_maintenance, working_area, created_at, updated_at';
|
|
48
51
|
|
|
49
52
|
// Shared column list for a goal_membership_requests row (task 1951 / ADR 0112) —
|
|
50
53
|
// the invite / join-request queue behind private goals. Every helper below shapes
|
|
@@ -260,7 +260,15 @@ async function applyGrade({ taskId, builderId, gradeResult, committedFiles = nul
|
|
|
260
260
|
// re-grade thrash is countable (task_grades is upsert-unique). Post-commit +
|
|
261
261
|
// best-effort: logGradeAttempt uses a pooled connection and swallows its own
|
|
262
262
|
// errors, so it can never roll back or block the grade that just committed.
|
|
263
|
-
|
|
263
|
+
// task 1004292: the per-worker verdicts (kind, verdict, error code, issue
|
|
264
|
+
// severities; no prose) ride the same row so an earlier round survives the
|
|
265
|
+
// re-grade that overwrites task_grades. Best-effort inside logGradeAttempt.
|
|
266
|
+
// `deps.attemptQuery` is the test-only seam (NODE_ENV==='test'), like deps.pool.
|
|
267
|
+
const attemptQuery = (deps.attemptQuery && process.env.NODE_ENV === 'test') ? deps.attemptQuery : undefined;
|
|
268
|
+
await logGradeAttempt({
|
|
269
|
+
taskId, builderId, passed: gradeResult.passed, score: gradeResult.score,
|
|
270
|
+
workers: gradeResult.signals && gradeResult.signals.workers,
|
|
271
|
+
}, attemptQuery);
|
|
264
272
|
|
|
265
273
|
return result;
|
|
266
274
|
}
|