Skip to content

feat(license-requests): profile+role tool mapping, approve-creates-assignment, copy-paste snippets (spec 032-v2) - #124

Merged
studert merged 4 commits into
mainfrom
worktree-request-workflow
Jul 9, 2026
Merged

feat(license-requests): profile+role tool mapping, approve-creates-assignment, copy-paste snippets (spec 032-v2)#124
studert merged 4 commits into
mainfrom
worktree-request-workflow

Conversation

@studert

@studert studert commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

Implements the revised spec 032 direction (see specs/032-automation-workflow/mockups-v2.html and implementation-plan-v2.html, review artifacts from 2026-07-09):

  • Ingest v2 — the Hub owns the tool decision. The Form/Power Automate payload now carries role (development|conception|business) + profile ("" = baseline | maxed | indie) + justification (required for maxed/indie), matched case-insensitively. The tool is derived via the new tool_mappings table — seeded from the AI Tooling Guide, editable under Settings → Tool Mapping. Indie resolves to needs decision (approver picks). The legacy toolName contract remains accepted so an un-updated PA flow keeps working during switchover.
  • Approve = assignment, provision-first. Approving records what the admin already provisioned in the vendor UI: one transaction auto-creates unknown requesters as Hub users (viewer, discipline from role, no invite), creates the encrypted license_assignments row, and stores the message. The Complete dialog/status is retired; legacy approved-without-assignment rows (REQ-001) get a one-shot Record assignment action. First-write-wins semantics preserved (race loss rolls back assignment and auto-created user).
  • Teams posting postponed → copy-paste snippets. Approve/reject flows end in a copy-ready message; the Messages card is a copy-paste log. Stored messages keep {{licenseCode}} unresolved — the key exists only encrypted on the assignment, masked in the log, decrypted on Reveal/Copy (admin-only). src/lib/teams/graph.ts is dormant (zero callers) for the future stack migration.
  • Queue/UX: role/profile badges, amber Needs decision, pending-only default facet, sidebar pending-count badge, API-key field only for requires_api_key tools (Claude Console).
  • Bug fixes: raw-SQL timestamp strings cast-not-converted (root cause of the audit's "search filters nothing"), form-payload label overlap + empty-answer clutter, sm:max-w-* dialog width overrides silently losing to the shadcn base class, Neon pool dead-idle-socket errors (idleTimeoutMillis).
  • Schema: migration 0029 — license_request_profile enum, 3 columns on license_requests, requested_tool_id nullable, ai_tools.requires_api_key, tool_mappings (partial unique indexes, same NULL handling as message_templates). Idempotent seeds: scripts/seed-tool-mappings.ts, scripts/seed-message-templates.ts (the template seed deferred since v1).

Test plan

  • 628 unit tests incl. new suites: mapping resolution, ingest schema case-insensitivity, {{licenseCode}} masking contract
  • 52 integration tests incl. the suites deferred since v1: ingest auth/idempotency/422s/legacy path; approve transaction (user auto-creation + rollback, duplicate-seat guard, concurrent first-write-wins, key encryption round-trip, message masking), recordAssignment, reject/cancel
  • Browser-verified end-to-end (24 checks, screenshots in the session log): v2 ingest → queue → detail → 2-step approve → snippet → auto-created user + assignment; indie needs-decision gating; tool-mapping settings; legacy REQ-001 rendering + record-assignment
  • pnpm lint && pnpm typecheck clean

Deploy notes

  1. Migration 0029 must run against production, then both seed scripts (idempotent).
  2. Set LICENSE_REQUEST_INGEST_SECRET in Vercel (the PA flow's bearer token).
  3. Update the Power Automate flow to send role/profile/justification from the updated Form (Title-Case values are fine — matching is case-insensitive). Until then the legacy tool-name path keeps working.

🤖 Generated with Claude Code

studert and others added 3 commits July 9, 2026 12:32
Delete the .specify directory (templates, PowerShell scripts,
constitution) and the nine .claude/commands/speckit.*.md slash
commands. Reword the forward-looking /speckit.* instructions in the
028/031 spec READMEs and the auto-generated CLAUDE.md header;
historical speckit mentions in completed spec docs are kept as-is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delete the post-sync Teams alert feature end to end: the evaluator,
card renderers, webhook poster, alert-state ledger and shared types in
src/lib/teams, the alert-only forecast-workspace engine and
loadCostHistory query, the evaluateAndPostTeamsAlerts hook in the
anthropic workspace cost sync, the TEAMS_WEBHOOK_URL /
TEAMS_DASHBOARD_BASE_URL env vars, and the spec 030 docs and unit
tests. Migration 0028 drops the anthropic_alert_state table.

The license-request Teams integration (spec 032 — graph.ts,
markdown.ts, teams_* columns) is intentionally untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…signment, copy-paste snippets (spec 032-v2)

Reworks the Microsoft Forms -> Hub license-request workflow per the revised
direction (specs/032-automation-workflow/{mockups,implementation-plan}-v2.html):

* Ingest v2: payload carries role + profile (+ required justification for
  maxed/indie, case-insensitive matching); the Hub derives the tool via the
  new tool_mappings table (seeded from the AI Tooling Guide, editable under
  Settings -> Tool Mapping). Legacy toolName contract still accepted during
  the Power Automate switchover.
* Approval is the terminal happy path (provision-first): one transaction
  auto-creates unknown requesters as viewer users (no invite), creates the
  encrypted license assignment, and stores the message. Complete dialog and
  status retired; legacy approved rows get a "Record assignment" action.
* Teams/Graph posting postponed: every message is a copy-paste snippet.
  Stored messages keep {{licenseCode}} unresolved - the audit log masks the
  key; Reveal/Copy decrypt on demand from the assignment. graph.ts dormant.
* Queue: role/profile column, needs-decision badge for indie, pending-only
  default facet, sidebar pending-count badge.
* Fixes: raw-SQL timestamps now converted for real (root cause of the
  "search filters nothing" bug), form-payload rendering (label overlap,
  empty answers collapsed), sm:max-w overrides on wide dialogs, Neon pool
  idle timeout (dead-socket errors after idle).
* Migration 0029 + idempotent seeds (tool mappings, default approval
  templates incl. API-key variant); unit + integration test suites for the
  mapping, ingest contract, and approval transaction (deferred since v1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 9, 2026 18:04
@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ai-developer-hub Ready Ready Preview, Comment Jul 9, 2026 6:14pm

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements spec 032-v2 changes to the license-request workflow: ingest now derives tools from (role, profile) mappings owned by the Hub, approval creates assignments (provision-first), and Teams posting is replaced by copy/paste snippets. It also removes the now-dormant Teams spend alert subsystem and introduces supporting schema, seeds, UI, and tests.

Changes:

  • Add (role, profile) → tool resolution via a new tool_mappings table + admin Settings UI, and update ingest validation/route logic to support v2 (while keeping legacy v1 toolName ingestion).
  • Retire Teams outbound posting in favor of copy-to-clipboard snippets, and remove the Claude spend Teams alerts modules/env/config/tests.
  • Add migration/seeds and update queue UX (pending-only default facet, role/profile badges, pending-count sidebar badge) plus Neon pool idle timeout fix.

Reviewed changes

Copilot reviewed 82 out of 85 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/unit/teams/webhook.test.ts Removed obsolete Teams webhook unit tests.
tests/unit/teams/format.test.ts Removed obsolete Teams formatting unit tests.
tests/unit/teams/forecast-workspace.test.ts Removed obsolete forecast unit tests tied to Teams alerts.
tests/unit/teams/evaluator-diff.test.ts Removed obsolete Teams alert evaluator unit tests.
tests/unit/teams/cards.test.ts Removed obsolete Adaptive Card rendering unit tests.
tests/unit/license-requests/render-template.test.ts Added test for {{licenseCode}} masking/unbound token behavior.
tests/unit/license-requests/mapping.test.ts Added unit tests for role/profile normalization + mapping resolution.
tests/unit/license-requests/ingest-schema.test.ts Added unit tests for case-insensitive role/profile validation and justification rules.
tests/integration/api/license-requests-ingest.test.ts Added integration tests for v2 ingest, auth, idempotency, and legacy v1 behavior.
src/types/ingestion.ts Extended ingestion details type to include v2 role/profile fields.
src/lib/validators.ts Updated ingest schema for v2 inputs; added tool-mapping/approval/record-assignment schemas; adjusted reject schema placement.
src/lib/teams/webhook.ts Removed Teams Workflows webhook poster implementation.
src/lib/teams/types.ts Removed Teams alerts shared types.
src/lib/teams/state.ts Removed Teams alerts state ledger helpers.
src/lib/teams/graph.ts Marked Teams Graph module as dormant (retained for future migration).
src/lib/teams/format.ts Removed Teams alerts formatting helpers.
src/lib/teams/evaluator.ts Removed Teams spend alert evaluator/orchestration.
src/lib/teams/cards.ts Removed Teams alerts adaptive card renderers.
src/lib/sync/sources/anthropic-workspace.ts Removed post-sync Teams alert evaluation call.
src/lib/license-requests/templates.ts Added listApprovalTemplates() to bulk-load approval templates for tool-switching UI.
src/lib/license-requests/mapping.ts Added pure mapping resolution utilities used by ingest and tests.
src/lib/env.ts Removed Teams alert env vars.
src/lib/db/schema.ts Added license_request_profile enum, requires_api_key, v2 request fields, and tool_mappings table + relations; removed alert-state table definition.
src/lib/db/migrations/meta/_journal.json Recorded migrations 0028/0029 in journal.
src/lib/db/migrations/0029_curly_strong_guy.sql Introduced schema changes for v2 mappings/profile + nullable requested_tool_id + requires_api_key.
src/lib/db/migrations/0028_dear_khan.sql Dropped Teams alert state table.
src/lib/db/index.ts Added Neon pool idle/connection timeouts to avoid dead idle sockets.
src/lib/anthropic/queries.ts Removed loadCostHistory type and function used by the deleted Teams alerts subsystem.
src/lib/anthropic/forecast-workspace.ts Removed forecast implementation used by the deleted Teams alerts subsystem.
src/components/data-table.tsx Added defaultSelected support for faceted filters (pending-only queue default).
src/components/copy-snippet.tsx Added reusable copy-to-clipboard button for snippet-based workflow.
src/components/app-sidebar.tsx Added pending-requests badge on Requests nav item.
src/app/settings/tool-mapping/tool-mapping-client.tsx Added Settings UI for editing tool mappings.
src/app/settings/tool-mapping/page.tsx Added Tool Mapping settings page (admin-only).
src/app/settings/settings-nav.tsx Added Tool Mapping tab to settings navigation.
src/app/settings/license-templates/template-editor-dialog.tsx Adjusted dialog width class to avoid shadcn base-class override issues.
src/app/requests/requests-table.tsx Added role/profile column + needs-decision tool badge + pending-only default filter.
src/app/requests/[id]/rejection-dialog.tsx Updated reject flow to end with copy/paste snippet rather than Teams send.
src/app/requests/[id]/record-assignment-dialog.tsx Added legacy “record assignment” dialog for v1-approved rows.
src/app/requests/[id]/page.tsx Switched to bulk-loading approval templates and updated props for new detail client flow.
src/app/requests/[id]/completion-dialog.tsx Removed retired completion dialog.
src/app/layout.tsx Added pending license request count query to drive sidebar badge for admins.
src/app/api/license-requests/ingest/route.ts Implemented v2 ingest derivation via tool mappings; removed fire-and-forget Teams Graph post.
src/actions/tool-mappings.ts Added server actions for listing/upserting/deleting tool mappings.
src/actions/license-templates.ts Removed unused recent form-keys helper after template handling changes.
specs/032-automation-workflow/implementation-notes.html Documented v2 decisions, deviations, and verification results.
specs/031-budget-workflow-and-redesign/README.md Removed speckit command reference from next steps.
specs/028-reports-v2/README.md Removed speckit command references; clarified next steps language.
scripts/seed-tool-mappings.ts Added idempotent seed for tool mappings + requires_api_key marking.
scripts/seed-message-templates.ts Added idempotent seed for approval message templates (seat vs API-key variants).
CLAUDE.md Tweaked header wording to reflect compilation source.
.specify/templates/tasks-template.md Removed specify template file.
.specify/templates/spec-template.md Removed specify template file.
.specify/templates/plan-template.md Removed specify template file.
.specify/templates/constitution-template.md Removed specify template file.
.specify/templates/checklist-template.md Removed specify template file.
.specify/templates/agent-file-template.md Removed specify template file.
.specify/scripts/powershell/setup-plan.ps1 Removed specify PowerShell script.
.specify/scripts/powershell/create-new-feature.ps1 Removed specify PowerShell script.
.specify/scripts/powershell/common.ps1 Removed specify PowerShell script.
.specify/scripts/powershell/check-prerequisites.ps1 Removed specify PowerShell script.
.specify/memory/constitution.md Removed specify constitution memory file.
.claude/commands/speckit.taskstoissues.md Removed speckit Claude command file.
.claude/commands/speckit.tasks.md Removed speckit Claude command file.
.claude/commands/speckit.specify.md Removed speckit Claude command file.
.claude/commands/speckit.plan.md Removed speckit Claude command file.
.claude/commands/speckit.implement.md Removed speckit Claude command file.
.claude/commands/speckit.constitution.md Removed speckit Claude command file.
.claude/commands/speckit.clarify.md Removed speckit Claude command file.
.claude/commands/speckit.analyze.md Removed speckit Claude command file.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib/validators.ts Outdated
Comment on lines +588 to +596
if (d.profile === "maxed" || d.profile === "indie") {
if (!d.justification || d.justification.trim().length === 0) {
ctx.addIssue({
code: "custom",
message: `justification is required when profile is "${d.profile}"`,
path: ["justification"],
});
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 23fdc03 — the justification requirement is now scoped to the v2 branch (role !== undefined), so a partially updated legacy caller sending a stray profile passes. Added a unit test covering exactly that case.

Comment on lines +108 to +127
async function handleSave() {
if (!editor) return;
setSaving(true);
const result = await upsertToolMapping({
role: editor.role === ANY_ROLE ? null : editor.role,
profile: editor.profile,
toolId: editor.toolId === NEEDS_DECISION ? null : Number(editor.toolId),
defaultTierId:
editor.toolId === NEEDS_DECISION || editor.defaultTierId === NO_TIER
? null
: Number(editor.defaultTierId),
});
setSaving(false);
if (result.success) {
setEditor(null);
router.refresh();
} else {
status.error(result.error);
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 23fdc03 — the server-action call is wrapped in try/catch/finally: saving always resets, and a thrown error surfaces as an inline status message instead of wedging the dialog.

Comment on lines 73 to +80
facetedFilters,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(() =>
(facetedFilters ?? [])
.filter((f) => f.defaultSelected && f.defaultSelected.length > 0)
.map((f) => ({ id: f.columnId, value: f.defaultSelected! })),
);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented in 23fdc03 (option a): defaultSelected applies via lazy state init at first mount only, and the JSDoc now states that facetedFilters must be present and stable at mount — later prop changes are deliberately ignored so user edits are never clobbered. All current consumers pass a static config, so effect-syncing would add risk without a consumer that needs it.

Comment thread src/app/requests/requests-table.tsx Outdated
Comment on lines +77 to +80
accessorFn: (row) =>
row.requesterRole
? `${ROLE_LABELS[row.requesterRole]} ${row.requesterProfile ? PROFILE_LABELS[row.requesterProfile] : ""}`
: "",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 23fdc03 — the accessor now builds the string with filter(Boolean).join(" "), so no trailing space when the profile is null.

…, recoverable mapping dialog, accessor/docs cleanups

* justification requirement only applies on the v2 branch (role present) —
  a partially updated legacy caller sending a stray profile no longer 400s
* tool-mapping editor resets its saving state in finally so a thrown server
  action can't wedge the dialog
* roleProfile queue accessor no longer emits a trailing space for rows
  without a profile
* document that DataTable defaultSelected applies at first mount only

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@studert
studert merged commit b138f0c into main Jul 9, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants